嵌入式Linux内核移植相关代码分析
2019-07-12 22:03 发布
生成海报
本文通过整理之前研发的一个项目(ARM7TDMI +uCLinux) ,分析内核启动过程及需要修改的文件,以供内核移植者参考。整理过程中也同时参考了众多网友的帖子,在此谢过。由于整理过程匆忙,难免错误及讲解的不够清楚之处,请各位网友指正,这里提前谢过。本文分以下部分进行介绍: 1. Bootloader 及内核解压 2. 内核启动方式介绍 3. 内核启动地址的确定 4. arch/armnommu/kernel/head-armv.S 分析 5. start_kernel() 函数分析 1. Bootloader 及内核解压 Bootloader 将内核加载到内存中,设定一些寄存器,然后将控制权交由内核,该过程中,关闭MMU 功能。通常,内核都是以压缩的方式存放,如zImage ,这里有两种解压方法: 使用内核自解压程序。 arch/arm/boot/compressed/head.S 或 arch/arm/boot/compressed/head-xxxxx.S arch/arm/boot/compressed/misc.c 在Bootloader 中增加解压功能。使用该方法时内核不需要带有自解压功能,而使用Bootloader 中的解压程序代替内核自解压程序。其工作过程与内核自解压过程相似:Bootloader 把压缩方式的内核解压到内存中,然后跳转到内核入口处开始执行。 2. 几种内核启动方式介绍 XIP (EXECUTE IN PLACE) 是指直接从存放代码的位置上启动运行。 2.1 非压缩,非 XIP 非XIP 方式是指在运行之前需对代码进行重定位。该类型的内核以非压缩方式存放在Flash 中,启动时由Bootloader 加载到内存后运行。2.2 非压缩, XIP 该类型的内核以非压缩格式存放在ROM/Flash 中,不需要加载到内存就能运行,Bootloader 直接跳转到其存放地址执行。Data 段复制和BSS 段清零的工作由内核自己完成。这种启动方式常用于内存空间有限的系统中,另外,程序在ROM/Flash 中运行的速度相对较慢。2.3 RAM 自解压 压缩格式的内核由开头一段自解压代码和压缩内核数据组成,由于以压缩格式存放,内核只能以非XIP 方式运行。RAM 自解压过程如下:压缩内核存放于ROM/Flash 中,Bootloader 启动后加载到内存中的临时空间,然后跳转到压缩内核入口地址执行自解压代码,内核被解压到最终的目的地址然后运行。压缩内核所占据的临时空间随后被Linux 回收利用。这种方式的内核在嵌入式产品中较为常见。 2.4 ROM 自解压 解压缩代码也能够以XIP 的方式在ROM/Flash 中运行。ROM 自解压过程如下:压缩内核存放在ROM/Flash 中,不需要加载到内存就能运行,Bootloader 直接跳转到其存放地址执行其自解压代码,将压缩内核解压到最终的目的地址并运行。ROM 自解压方式存放的内核解压缩速度慢,而且也不能节省内存空间。 3. 内核启动地址的确定 内核自解压方式 Head.S/head-XXX.S 获得内核解压后首地址ZREALADDR ,然后解压内核,并把解压后的内核放在ZREALADDR 的位置上,最后跳转到ZREALADDR 地址上,开始真正的内核启动。 arch/armnommu/boot/Makefile ,定义ZRELADDR 和ZTEXTADDR 。ZTEXTADDR 是自解压代码的起始地址,如果从内存启动内核,设置为0 即可,如果从Rom/Flash 启动,则设置ZTEXTADDR 为相应的值。ZRELADDR 是内核解压缩后的执行地址。 arch/armnommu/boot/compressed/vmlinux.ld, 引用LOAD_ADDR 和TEXT_START 。 arch/armnommu/boot/compressed/Makefile, 通过如下一行: SEDFLAGS = s/TEXT_START/$(ZTEXTADDR)/;s/LOAD_ADDR/$(ZRELADDR)/; 使得TEXT_START = ZTEXTADDR ,LOAD_ADDR = ZRELADDR 。 说明: 执行完decompress_kernel 函数后, 代码跳回head.S/head-XXX.S 中, 检查解压缩之后的kernel 起始地址是否紧挨着kernel image 。如果是,beqcall_kernel, 执行解压后的kernel 。如果解压缩之后的kernel 起始地址不是紧挨着kernelimage, 则执行relocate, 将其拷贝到紧接着kernel image 的地方, 然后跳转, 执行解压后的kernel 。 Bootloader 解压方式 Bootloader 把解压后的内核放在内存的TEXTADDR 位置上,然后跳转到TEXTADDR 位置上,开始内核启动。 arch/armnommu/Makefile ,一般设置TEXTADDR 为PAGE_OFF+0x8000 ,如定义为0x00008000, 0xC0008000 等。 arch/armnommu/vmlinux.lds ,引用 TEXTADDR 4. arch/armnommu/kernel/head-armv.S 该文件是内核最先执行的一个文件,包括内核入口ENTRY(stext) 到start_kernel 间的初始化代码,主要作用是检查CPUID ,Architecture Type ,初始化BSS 等操作,并跳到start_kernel 函数。在执行前,处理器应满足以下状态:r0 - should be 0 r1 - unique architecture number MMU - off I-cache - on or off D-cache – off /* 部分源代码分析 */ /* 内核入口点 */ ENTRY(stext) /* 程序状态,禁止FIQ 、IRQ ,设定SVC 模式 */ mov r0, #F_BIT | I_BIT | MODE_SVC@ make sure svc mode /* 置当前程序状态寄存器 */ msr cpsr_c, r0 @ and all irqs disabled /* 判断CPU 类型,查找运行的CPU ID 值与Linux 编译支持的ID 值是否支持 */ bl __lookup_processor_type /* 跳到__error */ teq r10, #0 @ invalid processor? moveq r0, #'p' @ yes, error 'p' beq __error /* 判断体系类型,查看R1 寄存器的Architecture Type 值是否支持 */ bl __lookup_architecture_type /* 不支持,跳到出错 */ teq r7, #0 @ invalid architecture? moveq r0, #'a' @ yes, error 'a' beq __error /* 创建核心页表 */ bl __create_page_tables adr lr, __ret @ return address add pc, r10, #12 @ initialise processor /* 跳转到start_kernel 函数 */ b start_kernel __lookup_processor_type 这个函数根据芯片的ID 从proc.info 获取proc_info_list 结构,proc_info_list 结构定义在include/asm-armnommu/proginfo.h 中,该结构的数据定义在arch/armnommu/mm/proc-arm*.S 文件中,ARM7TDMI 系列芯片的proc_info_list 数据定义在arch/armnommu/mm/proc-arm6,7.S 文件中。函数__lookup_architecture_type 从arch.info 获取machine_desc 结构,machine_desc 结构定义在include/asm-armnommu/mach/arch.h 中,针对不同arch 的数据定义在arch/armnommu/mach-*/arch.c 文件中。在这里如果知道processor_type 和architecture_type, 可以直接对相应寄存器进行赋值。 5. start_kernel() 函数分析 下面对start_kernel() 函数及其相关函数进行分析。 5.1 lock_kernel() /* Getting the big kernel lock. * This cannot happen asynchronously, * so we only need to worry about other * CPU's. */ extern __inline__ void lock_kernel(void) { if (!++current->lock_depth) spin_lock(&kernel_flag); } kernel_flag 是一个内核大自旋锁,所有进程都通过这个大锁来实现向内核态的迁移。只有获得这个大自旋锁的处理器可以进入内核,如中断处理程序等。在任何一对lock_kernel /unlock_kernel 函数里至多可以有一个程序占用CPU 。进程的lock_depth 成员初始化为-1 ,在kerenl/fork.c 文件中设置。在它小于0 时(恒为-1 ),进程不拥有内核锁;当大于或等于0 时,进程得到内核锁。 5.2 setup_arch() setup_arch() 函数做体系相关的初始化工作,函数的定义在arch/armnommu/kernel/setup.c 文件中,主要涉及下列主要函数及代码。 5.2.1 setup_processor() 该函数主要通过 for (list = &__proc_info_begin; list < &__proc_info_end ; list++) if ((processor_id & list->cpu_mask) == list->cpu_val) break; 这样一个循环来在.proc.info 段中寻找匹配的processor_id ,processor_id 在head_armv.S 文件 中设置。 5.2.2 setup_architecture(machine_arch_type) 该函数获得体系结构的信息,返回mach-xxx/arch.c 文件中定义的machine 结构体的指针,包含以下内容: MACHINE_START (xxx, “xxx”) MAINTAINER ("xxx") BOOT_MEM (xxx, xxx, xxx) FIXUP (xxx) MAPIO (xxx) INITIRQ (xxx) MACHINE_END 5.2.3 内存设置代码 if (meminfo.nr_banks == 0) { meminfo.nr_banks = 1; meminfo.bank[0].start = PHYS_OFFSET; meminfo.bank[0].size = MEM_SIZE; } meminfo 结构表明内存情况,是对物理内存结构meminfo 的默认初始化。nr_banks 指定内存块的数量,bank 指定每块内存的范围,PHYS_OFFSET 指定某块内存块的开始地址,MEM_SIZE 指定某块内存块长度。PHYS_OFFSET 和MEM_SIZE 都定义在include/asm-armnommu/arch-XXX/memory.h 文件中,其中PHYS_OFFSET 是内存的开始地址,MEM_SIZE 就是内存的结束地址。这个结构在接下来内存的初始化代码中起重要作用。 5.2.4 内核内存空间管理 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 定义了整个内核的内存空间,内核线程属于内核代码,同样使用内核空间,其访问内存空间的权限与内核一样。5.2.5 内存结构初始化 bootmem_init(&meminfo) 函数根据meminfo 进行内存结构初始化。bootmem_init(&meminfo) 函数中调用reserve_node_zero(bootmap_pfn, bootmap_pages) 函数,这个函数的作用是保留一部分内存使之不能被动态分配。这些内存块包括: reserve_bootmem_node(pgdat, __pa(&_stext), &_end - &_stext); /* 内核所占用地址空间*/ reserve_bootmem_node(pgdat, bootmap_pfn< /*bootmem 结构所占用地址空间*/ 5.2.6 paging_init(&meminfo, mdesc) 创建内核页表,映射所有物理内存和IO 空间,对于不同的处理器,该函数差别比较大。下面简单描述一下ARM 体系结构的存储系统及MMU 相关的概念。在ARM 存储系统中,使用内存管理单元(MMU) 实现虚拟地址到实际物理地址的映射。利用MMU ,可把SDRAM 的地址完全映射到0x0 起始的一片连续地址空间,而把原来占据这片空间的FLASH 或者ROM 映射到其他不相冲突的存储空间位置。例如,FLASH 的地址从0x00000000 ~0x00FFFFFF ,而SDRAM 的地址范围是0x3000 0000 ~0x3lFFFFFF ,则可把SDRAM 地址映射为0x00000000 ~0xlFFFFFF ,而FLASH 的地址可以映射到0x90000000 ~0x90FFFFFF( 此处地址空间为空闲,未被占用) 。映射完成后,如果处理器发生异常,假设依然为IRQ 中断,PC 指针指向0xl8 处的地址,而这个时候PC 实际上是从位于物理地址的0x30000018 处读取指令。通过MMU 的映射,则可实现程序完全运行在SDRAM 之中。在实际的应用中.可能会把两片不连续的物理地址空间分配给SDRAM 。而在操作系统中,习惯于把SDRAM 的空间连续起来,方便内存管理,且应用程序申请大块的内存时,操作系统内核也可方便地分配。通过MMU 可实现不连续的物理地址空间映射为连续的虚拟地址空间。操作系统内核或者一些比较关键的代码,一般是不希望被用户应用程序访问。通过MMU 可以控制地址空间的访问权限,从而保护这些代码不被破坏。 MMU 的实现过程,实际上就是一个查表映射的过程。建立页表是实现MMU 功能不可缺少的一步。页表位于系统的内存中,页表的每一项对应于一个虚拟地址到物理地址的映射。每一项的长度即是一个字的长度( 在ARM 中,一个字的长度被定义为4Bytes) 。页表项除完成虚拟地址到物理地址的映射功能之外,还定义了访问权限和缓冲特性等。 MMU 的映射分为两种,一级页表的变换和二级页表变换。两者的不同之处就是实现的变换地址空间大小不同。一级页表变换支持1 M 大小的存储空间的映射,而二级可以支持64 kB ,4 kB 和1 kB 大小地址空间的映射。 动态表( 页表) 的大小=表项数*每个表项所需的位数,即为整个内存空间建立索引表时,需要多大空间存放索引表本身。 表项数=虚拟地址空间/ 每页大小 每个表项所需的位数=Log( 实际页表数)+ 适当控制位数 实际页表数 =物理地址空间/ 每页大小 下面分析paging_init ()函数的代码。 在paging_init 中分配起始页(即第0 页)地址: zero_page = 0xCXXXXXXX memtable_init(mi); 如果当前微处理器带有MMU ,则为系统内存创建页表;如果当前微处理器不支持MMU ,比如ARM7TDMI 上移植uCLinux 操作系统时,则不需要此类步骤。可以通过如下一个宏定义实现灵活控制,对于带有MMU 的微处理器而言,memtable_init(mi) 是paging_init() 中最重要的函数。 #ifndef CONFIG_UCLINUX /* initialise the page tables. */ memtable_init(mi); …… (此处省略若干代码) free_area_init_node(node, pgdat, 0, zone_size, bdata->node_boot_start, zhole_size); } #else /* 针对不带MMU 微处理器 */ { /*****************************************************/ 定义物理内存区域管理/*****************************************************/ unsigned long zone_size[MAX_NR_ZONES] = {0,0,0}; zone_size[ZONE_DMA] = 0; zone_size[ZONE_NORMAL] = (END_MEM - PAGE_OFFSET) >> PAGE_SHIFT; free_area_init_node(0, NULL, NULL, zone_size, PAGE_OFFSET, NULL); } #endif uCLinux 与其它嵌入式Linux 最大的区别就是MMU 管理这一块,从上面代码就明显可以看到这点区别。下面继续讨论针对带MMU 的微处理器的内存管理。 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 // 空 };init_maps /* map_desc 是区段及其属性的定义 */ 下面代码对meminfo 的区段进行遍历,在嵌入式系统中列举所有可映射的内存,例如32M SDRAM, 4M FLASH 等,用meminfo 记录这些内存区段。同时填写init_maps 中的各项内容。meminfo 结构如下:struct meminfo { int nr_banks; unsigned long end; struct { unsigned long start; unsigned long size; int node; } bank[NR_BANKS]; }; /********************************************************/ 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 /***********************************************************/ 接下来的代码是逐个区段建立页表/***********************************************************/ q = init_maps; do { if (address < q->virtual || q == p) { /*******************************************************************************/ 由于内核空间是从某个地址开始,如0xC0000000 ,所以0xC000 0000 以前的页表项全部清空 clear_mapping 在mm-armv.c 中定义,其中clear_mapping() 是个宏,根据处理器的不同,可以被展开为如下代码 cpu_XXX_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 中定义。cpu_XXX_set_pmd 定义在 proc_armXXX.S 文件中,参见ENTRY(cpu_XXX_set_pmd) 处代码。 /*********************************************************************************/ clear_mapping(address); /* 每个表项增加1M */ address += PGDIR_SIZE; } 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; /*******************************************************************************/ 大部分应用中均采用1 级section 模式的地址映射,一个section 的大小为1M ,也就是说从逻辑地址到物理地址的转变是这样的一个过程:一个32 位的地址,高12 位决定了该地址在页表中的index ,这个index 的内容决定了该逻辑section 对应的物理section ;低20 位决定了该地址在section 中的偏移(index )。例如:从0x0 ~0xFFFFFFFF 的地址空间总共可以分成0x1000 (4K )个 section (每个section 大小为1M ),页表中每项的大小为32 个bit ,因此页表的大小为0x4000 (16K )。 每个页表项的内容如下 : bit: 31 20 19 12 11 10 9 8 5 4 3 2 1 0 content: Section 对应的物理地址 NULL AP 0 Domain 1 C B 1 0 最低两位(10 )是section 分页的标识。AP :Access Permission ,区分只读、读写、SVC &其它模式。 Domain :每个section 都属于某个Domain ,每个Domain 的属性由寄存器控制。一般都只要包含两个Domain ,一个可访问地址空间;另一个不可访问地址空间。 C 、B :这两位决定了该section 的cache &write buffer 属性,这与该段的用途(RO or RW) 有密切关系。不同的用途要做不同的设置。 C B 具体含义 0 0 无cache ,无写缓冲,任何对memory 的读写都反映到总线上。对 memory 的操作过程中CPU 需要等待。 0 1 无cache ,有写缓冲,读操作直接反映到总线上。写操作CPU 将数据写入到写缓冲后继续运行,由写缓冲进行写回操作。 1 0 有cache ,写通模式,读操作首先考虑cache hit ;写操作时直接将数据写入写缓冲,如果同时出现cache hit ,那么也更新cache 。 1 1 有cache ,写回模式,读操作首先考虑cache hit ;写操作也首先考虑cache hit 。 由于ARM 中section 表项的权限位和page 表项的位置不同, 以下代码根据struct map_desc 中的保护标志,分别计算页表项中的AP, Domain 和CB 标志位。 /*******************************************************************************/ prot_pte = L_PTE_PRESENT | L_PTE_YOUNG | L_PTE_DIRTY | (md->prot_readprot_writecacheablebufferable prot_sect = PMD_TYPE_SECT | PMD_DOMAIN(md->domain) | (md->prot_readprot_writecacheablebufferable /********************************************************************/ 设置虚拟地址,偏移地址和内存 length /********************************************************************/ virt = md->virtual; off = md->physical - virt; length = md->length; --------------------------------------------------------------------------------------------------- 建立虚拟地址到物理地址的映射/********************************************************************/ 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; } /*************************************************************************/ create_mapping 的作用是设置虚地址virt 到物理地址virt + off_set 的映射页目录和页表。 /*************************************************************************/ /* 映射中断向量表区域 */ 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 分配的,通常是在PAGE_OFF+0x8000 前面的某一页,vectors_base() 是个宏,ARM 规定中断向量表的地址只能是0 或0xFFFF0000 ,所以上述代码映射一页到0 或0xFFFF0000 ,中断处理程序中的部分代码也被拷贝到这一页中。5.3 parse_options() 分析由内核引导程序发送给内核的启动选项,在初始化过程中按照某些选项运行,并将剩余部分传送给init 进程。这些选项可能已经存储在配置文件中,也可能是由用户在系统启动时敲入的。但内核并不关心这些,这些细节都是内核引导程序关注的内容,嵌入式系统更是如此。 5.4 trap_init() 这个函数用来做体系相关的中断处理的初始化,在该函数中调用__trap_init((void*)vectors_base()) 函数将exceptionvector 设置到vectors_base 开始的地址上。__trap_init 函数位于entry-armv.S 文件中,对于ARM 处理器,共有复位、未定义指令、SWI 、预取终止、数据终止、IRQ 和FIQ 几种方式。SWI 主要用来实现系统调用,而产生了IRQ 之后,通过exceptionvector 进入中断处理过程,执行do_IRQ 函数。 armnommu 的trap_init ()函数在arch/armnommu/kernel/traps.c 文件中。vectors_base 是写中断向量的开始地址,在include/asm-armnommu/proc-armv/system.h 文件中设置,地址为0 或0XFFFF0000 。 ENTRY(__trap_init) stmfd sp!, {r4 - r6, lr} mrs r1, cpsr @ code from 2.0.38 bic r1, r1, #MODE_MASK @ clear mode bits /* 设置svc 模式, disable IRQ,FIQ */ orr r1, r1, #I_BIT|F_BIT|MODE_SVC @ set SVC mode, disable IRQ,FIQ msr cpsr, r1 adr r1, .LCvectors @ set up the vectors ldmia r1, {r1, r2, r3, r4, r5, r6, ip, lr} stmia r0, {r1, r2, r3, r4, r5, r6, ip, lr} /* 拷贝异常向量 */ add r2, r0, #0x200 adr r0, __stubs_start @ copy stubs to 0x200 adr r1, __stubs_end 1: ldr r3, [r0], #4 str r3, [r2], #4 cmp r0, r1 blt 1b LOADREGS(fd, sp!, {r4 - r6, pc}) __stubs_start 到__stubs_end 的地址中包含了异常处理的代码,因此拷贝到vectors_base+0x200 的位置上。5.5 init_IRQ() void __init init_IRQ(void) { extern void init_dma(void); int irq; for (irq = 0; irq < NR_IRQS; irq++) { irq_desc[irq].probe_ok = 0; irq_desc[irq].valid = 0; irq_desc[irq].noautoenable = 0; irq_desc[irq].mask_ack = dummy_mask_unmask_irq; irq_desc[irq].mask = dummy_mask_unmask_irq; irq_desc[irq].unmask = dummy_mask_unmask_irq; } CSR_WRITE(AIC_MDCR, 0x7FFFE); /* disable all interrupts */ CSR_WRITE(CAHCNF,0x0);/*Close Cache*/ CSR_WRITE(CAHCON,0x87);/*Flush Cache*/ while(CSR_READ(CAHCON)!=0); CSR_WRITE(CAHCNF,0x7);/*Open Cache*/ init_arch_irq(); init_dma(); } 这个函数用来做体系相关的irq 处理的初始化,irq_desc 数组是用来描述IRQ 的请求队列,每一个中断号分配一个irq_desc 结构,组成了一个数组。NR_IRQS 代表中断数目,这里只是对中断结构irq_desc 进行了初始化。在默认的初始化完成后调用初始化函数init_arch_irq ,先执行arch/armnommu/kernel/irq-arch.c 文件中的函数genarch_init_irq() ,然后就执行include/asm-armnommu/arch-xxxx/irq.h 中的inline 函数irq_init_irq ,在这里对irq_desc 进行了实质的初始化。其中mask 用阻塞中断;unmask 用来取消阻塞;mask_ack 的作用是阻塞中断,同时还回应ack 给硬件表示这个中断已经被处理了,否则硬件将再次发生同一个中断。这里,不是所有硬件需要这个ack 回应,所以很多时候mask_ack 与mask 用的是同一个函数。 接下来执行init_dma ()函数,如果不支持DMA ,可以设置include/asm-armnommu/arch-xxxx/dma.h 中的MAX_DMA_CHANNELS 为0 ,这样在arch/armnommu/kernel/dma.c 文件中会根据这个定义使用不同的函数。 5.6 sched_init() 初始化系统调度进程,主要对定时器机制和时钟中断的BottomHalf 的初始化函数进行设置。与时间相关的初始化过程主要有两步:(1 )调用init_timervecs() 函数初始化内核定时器机制;(2 )调用init_bh() 函数将BH 向量TIMER_BH 、TQUEUE_BH 和IMMEDIATE_BH 所对应的BH 函数分别设置成timer_bh() 、tqueue_bh() 和immediate_bh() 函数 5.7 softirq_init() 内核的软中断机制初始化函数。调用tasklet_init 初始化tasklet_struct 结构,软中断的个数为32 个。用于bh 的tasklet_struct 结构调用tasklet_init() 以后,它们的函数指针func 全都指向bh_action() 。bh_action 就是tasklet 实现bh 机制的代码,但此时具体的bh 函数还没有指定。 HI_SOFTIRQ 用于实现bottom half ,TASKLET_SOFTIRQ 用于公共的tasklet 。 open_softirq(TASKLET_SOFTIRQ, tasklet_action, NULL); /* 初始化公共的tasklet_struct 要用到的软中断 */ open_softirq(HI_SOFTIRQ, tasklet_hi_action, NULL); /* 初始化tasklet_struct 实现的bottom half 调用 */ 这里顺便讲一下软中断的执行函数do_softirq() 。软中断服务不允许在一个硬中断服务程序内部执行,也不允许在一个软中断服务程序内部执行,所以通过in_interrupt() 加以检查。h->action 就是串行化执行软中断,当bh 的tasklet_struct 链入的时候,就能在这里执行,在bh 里重新锁定了所有CPU ,导致一个时间只有一个CPU 可以执行bh 函数,但是do_softirq() 是可以在多CPU 上同时执行的。而每个tasklet_struct 在一个时间上是不会出现在两个CPU 上的。另外,只有当Linux 初始化完成开启中断后,中断系统才可以开始工作。 5.8 time_init() 这个函数用来做体系相关的timer 的初始化,armnommu 的在arch/armnommu/kernel/time.c 。这里调用了在include/asm-armnommu/arch-xxxx/time.h 中的inline 函数setup_timer ,setup_timer ()函数的设计与硬件设计紧密相关,主要是根据硬件设计情况设置时钟中断号和时钟频率等。 void __inline__ setup_timer (void) { /*----- disable timer -----*/ CSR_WRITE(TCR0, xxx); CSR_WRITE (AIC_SCR7, xxx); /* setting priority level to high */ /* timer 0: 100 ticks/sec */ CSR_WRITE(TICR0, xxx); timer_irq.handler = xxxxxx_timer_interrupt; setup_arm_irq(IRQ_TIMER, &timer_irq); /* IRQ_TIMER is the interrupt number */ INT_ENABLE(IRQ_TIMER); /* Clear interrupt flag */ CSR_WRITE(TISR, xxx); /* enable timer */ CSR_WRITE(TCR0, xxx); } 5.9 console_init() 控制台初始化。控制台也是一种驱动程序,由于其特殊性,提前到该处完成初始化,主要是为了提前看到输出信息,据此判断内核运行情况。很多嵌入式Linux 操作系统由于没有在/dev 目录下正确配置console 设备,造成启动时发生诸如unable to open an initialconsole 的错误。 /*******************************************************************************/ init_modules() 函数到smp_init() 函数之间的代码一般不需要作修改, 如果平台具有特殊性,也只需对相关函数进行必要修改。 这里简单注明了一下各个函数的功能,以便了解。 /*******************************************************************************/ 5.10 init_modules() 模块初始化。如果编译内核时使能该选项,则内核支持模块化加载/ 卸载功能 5.11 kmem_cache_init() 内核Cache 初始化。 5.12 sti() 使能中断,这里开始,中断系统开始正常工作 --------------------------------------------------------------------------------------------------- 5.13 calibrate_delay() 近似计算BogoMIPS 数字的内核函数。作为第一次估算,calibrate_delay 计算出在每一秒内执行多少次__delay 循环,也就是每个定时器滴答(timer tick )― 百分之一秒内延时循环可以执行多少次。这种计算只是一种估算,结果并不能精确到纳秒,但这个数字供内核使用已经足够精确了。 BogoMIPS 的数字由内核计算并在系统初始化的时候打印。它近似的给出了每秒钟CPU 可以执行一个短延迟循环的次数。在内核中,这个结果主要用于需要等待非常短周期的设备驱动程序―― 例如,等待几微秒并查看设备的某些信息是否已经可用。 计算一个定时器滴答内可以执行多少次循环需要在滴答开始时就开始计数,或者应该尽可能与它接近。全局变量jiffies 中存储了从内核开始保持跟踪时间开始到现在已经经过的定时器滴答数, jiffies 保持异步更新,在一个中断内—— 每秒一百次,内核暂时挂起正在处理的内容,更新变量,然后继续刚才的工作。 5.14 mem_init() 内存初始化。本函数通过内存碎片的重组等方法标记当前剩余内存, 设置内存上下界和页表项初始值。 5.15 kmem_cache_sizes_init() 内核内存管理器的初始化,也就是初始化cache 和SLAB 分配机制。 5.16 pgtable_cache_init() 页表cache 初始化。 5.17 fork_init() 这里根据硬件的内存情况,如果计算出的max_threads 数量太大,可以自行定义。 5.18 proc_caches_init(); 为proc 文件系统创建高速缓冲 5.19 vfs_caches_init(num_physpages); 为VFS 创建SLAB 高速缓冲 5.20 buffer_init(num_physpages); 初始化 buffer 5.21 page_cache_init(num_physpages); 页缓冲初始化5.22 signals_init(); 创建信号队列高速缓冲 5.23 proc_root_init(); 在内存中创建包括根结点在内的所有节点 5.24 check_bugs(); 检查与处理器相关的 bug 5.25 smp_init(); 5.26 rest_init(); 此函数调用kernel_thread(init, NULL, CLONE_FS | CLONE_FILES | CLONE_SIGNAL) 函数。5.26.1 kernel_thread() 函数分析 这里调用了arch/armnommu/kernel/process.c 中的函数kernel_thread ,kernel_thread 函数中通过 __syscall(clone) 创建新线程。__syscall(clone) 函数参见armnommu/kernel 目录下的entry-common.S 文件。 5.26.2 init() 完成下列功能: Init() 函数通过kernel_thread(init, NULL, CLONE_FS | CLONE_FILES | CLONE_SIGNAL) 的回调函数执行,完成下列功能。 do_basic_setup() 在该函数里,sock_init() 函数进行网络相关的初始化,占用相当多的内存,如果所开发系统不支持网络功能,可以把该函数的执行注释掉。 do_initcalls() 实现驱动的初始化,
打开微信“扫一扫”,打开网页后点击屏幕右上角分享按钮