Linux驱动开发:Linux内核启动流程详解(1)

前言:Linux 内核同样作为 Linux 驱动开发的**“三巨头”**之一,Linux 内核的启动流程要比 uboot 复杂的多,涉及到的内容也更多。但秉持着  “知其然知其所以然”的学习态度,作者将给读者朋友大致的过一遍 Linux 内核的启动流程。(考虑到硬件平台与Linux内核版本不一致,实际情况可能有些许出入)

实验硬件:imx6ull;Linux内核版本:4.1.15

想深挖 Linux 内核的启动流程就需要从 Linux 的链接脚本入手(程序的入口:程序执行的第一条指令被称为程序的入口,这个入口通常就是在链接脚本指定的),打开 arch/arm/kernel/vmlinux.lds这个文件(下载 Linux 源码后进行编译得到lds链接脚本):

/\* ld script to make ARM Linux kernel
 \* taken from the i386 version by Russell King
 \* Written by Martin Mares <mj@atrey.karlin.mff.cuni.cz>
 \*/

#include <asm-generic/vmlinux.lds.h>
#include <asm/cache.h>
#include <asm/thread\_info.h>
#include <asm/memory.h>
#include <asm/page.h>
#ifdef CONFIG\_ARM\_KERNMEM\_PERMS
#include <asm/pgtable.h>
#endif

#define PROC\_INFO \
 . = ALIGN(4); \
 VMLINUX\_SYMBOL(\_\_proc\_info\_begin) = .; \
 \*(.proc.info.init) \
 VMLINUX\_SYMBOL(\_\_proc\_info\_end) = .;

#define IDMAP\_TEXT \
 ALIGN\_FUNCTION(); \
 VMLINUX\_SYMBOL(\_\_idmap\_text\_start) = .; \
 \*(.idmap.text) \
 VMLINUX\_SYMBOL(\_\_idmap\_text\_end) = .; \
 . = ALIGN(PAGE\_SIZE); \
 VMLINUX\_SYMBOL(\_\_hyp\_idmap\_text\_start) = .; \
 \*(.hyp.idmap.text) \
 VMLINUX\_SYMBOL(\_\_hyp\_idmap\_text\_end) = .;

#ifdef CONFIG\_HOTPLUG\_CPU
#define ARM\_CPU\_DISCARD(x)
#define ARM\_CPU\_KEEP(x) x
#else
#define ARM\_CPU\_DISCARD(x) x
#define ARM\_CPU\_KEEP(x)
#endif

#if (defined(CONFIG\_SMP\_ON\_UP) && !defined(CONFIG\_DEBUG\_SPINLOCK)) || \
 defined(CONFIG\_GENERIC\_BUG)
#define ARM\_EXIT\_KEEP(x) x
#define ARM\_EXIT\_DISCARD(x)
#else
#define ARM\_EXIT\_KEEP(x)
#define ARM\_EXIT\_DISCARD(x) x
#endif

OUTPUT\_ARCH(arm)
ENTRY(stext)

#ifndef \_\_ARMEB\_\_
jiffies = jiffies_64;
#else
jiffies = jiffies_64 + 4;
#endif

SECTIONS
{
	/\*
 \* XXX: The linker does not define how output sections are
 \* assigned to input sections when there are multiple statements
 \* matching the same input section name. There is no documented
 \* order of matching.
 \*
 \* unwind exit sections must be discarded before the rest of the
 \* unwind sections get included.
 \*/
	/DISCARD/ : {
		*(.ARM.exidx.exit.text)
		*(.ARM.extab.exit.text)
		ARM\_CPU\_DISCARD(*(.ARM.exidx.cpuexit.text))
		ARM\_CPU\_DISCARD(*(.ARM.extab.cpuexit.text))
		ARM\_EXIT\_DISCARD(EXIT_TEXT)
		ARM\_EXIT\_DISCARD(EXIT_DATA)
		EXIT_CALL
#ifndef CONFIG\_MMU
		*(.text.fixup)
		*(__ex_table)
#endif
#ifndef CONFIG\_SMP\_ON\_UP
		*(.alt.smp.init)
#endif
		*(.discard)
		*(.discard.*)
	}

#ifdef CONFIG\_XIP\_KERNEL
	. = XIP\_VIRT\_ADDR(CONFIG_XIP_PHYS_ADDR);
#else
	. = PAGE_OFFSET + TEXT_OFFSET;
#endif
	.head.text : {
		_text = .;
		HEAD_TEXT
	}

#ifdef CONFIG\_ARM\_KERNMEM\_PERMS
	. = ALIGN(1<<SECTION_SHIFT);
#endif

//省略.......

在 vmlinux.lds 链接文件的第 49 行可以找到 ENTRY(stext) ,ENTRY 指明了了 Linux 内核入口,入口为 stextstext 定义在文件 arch/arm/kernel/head.S 中 , 因 此 要 分 析 Linux 内核的启动流程,就得先从文件 arch/arm/kernel/head.Sstext 处开始分析。

一、Linux内核总体启动流程

★作者将 Linux 内核的总体启动流程分为 5 个部分:

二、Linux内核启动流程

2.1 Linux 内核入口 stext

stext 是 Linux 内核的入口地址,在文件 arch/arm/kernel/head.S 中有如下所示提示内容:

/*
 * Kernel startup entry point.
 * ---------------------------
 *
 * This is normally called from the decompressor code.  The requirements
 * are: MMU = off, D-cache = off, I-cache = dont care, r0 = 0,
 * r1 = machine nr, r2 = atags or dtb pointer.
 *
 * This code is mostly position independent, so if you link the kernel at
 * 0xc0008000, you call this at __pa(0xc0008000).
 *
 * See linux/arch/arm/tools/mach-types for the complete list of machine
 * numbers for r1.
 *
 * We're trying to keep crap to a minimum; DO NOT add any machine specific
 \* crap here - that's what the boot loader (or in extreme, well justified
 * circumstances, zImage) is for.
 */

根据上方示例代码中的注释,Linux 内核启动之前要求如下:
①、关闭 MMU。
②、关闭 D-cache。
③、I-Cache 无所谓。
④、r0 = 0。
⑤、r1= machine nr(也就是机器 ID)。
⑥、r2 = atags 或者设备树(dtb)首地址。
Linux 内核的入口点 stext 其实相当于内核的入口函数,stext 函数内容如下:

ENTRY(stext)
 ARM\_BE8(setend	be )			@ ensure we are in BE8 mode

 THUMB( adr r9, BSYM(1f) ) @ Kernel is always entered in ARM.
 THUMB( bx r9 ) @ If this is a Thumb-2 kernel,
 THUMB( .thumb ) @ switch to Thumb now.
 THUMB(1: )

#ifdef CONFIG\_ARM\_VIRT\_EXT
 bl \_\_hyp\_stub\_install
#endif
 @ ensure svc mode and all interrupts masked
 safe\_svcmode\_maskall r9

 mrc p15, 0, r9, c0, c0 @ get processor id
 bl \_\_lookup\_processor\_type @ r5=procinfo r9=cpuid
	movs	r10, r5				@ invalid processor (r5=0)?
 THUMB( it	eq )		@ force fixup-able long branch encoding
	beq	__error_p			@ yes, error 'p'

#ifdef CONFIG\_ARM\_LPAE
	mrc	p15, 0, r3, c0, c1, 4		@ read ID_MMFR0
	and	r3, r3, #0xf			@ extract VMSA support
	cmp	r3, #5				@ long-descriptor translation table format?
 THUMB( it	lo )				@ force fixup-able long branch encoding
	blo	__error_lpae			@ only classic page table format
#endif

#ifndef CONFIG\_XIP\_KERNEL
	adr	r3, 2f
	ldmia	r3, {r4, r8}
	sub	r4, r3, r4			@ (PHYS_OFFSET - PAGE_OFFSET)
	add	r8, r8, r4			@ PHYS_OFFSET
#else
	ldr	r8, =PLAT_PHYS_OFFSET		@ always constant in this case
#endif

	/\*
 \* r1 = machine no, r2 = atags or dtb,
 \* r8 = phys\_offset, r9 = cpuid, r10 = procinfo
 \*/
	bl	__vet_atags
#ifdef CONFIG\_SMP\_ON\_UP
	bl	__fixup_smp
#endif
#ifdef CONFIG\_ARM\_PATCH\_PHYS\_VIRT
	bl	__fixup_pv_table
#endif
	bl	__create_page_tables

	/\*
 \* The following calls CPU specific code in a position independent
 \* manner. See arch/arm/mm/proc-\*.S for details. r10 = base of
 \* xxx\_proc\_info structure selected by \_\_lookup\_processor\_type
 \* above. On return, the CPU will be ready for the MMU to be
 \* turned on, and r0 will hold the CPU control register value.
 \*/
	ldr	r13, =__mmap_switched		@ address to jump to after
						@ mmu has been enabled
	adr	lr, BSYM(1f)			@ return (PIC) address
	mov	r8, r4				@ set TTBR1 to swapper_pg_dir
	ldr	r12, [r10, #PROCINFO_INITFUNC]
	add	r12, r12, r10
	ret	r12
1:	b	__enable_mmu
ENDPROC(stext)

在 stext 函数中进行了如下流程:

①、调用函数 safe_svcmode_maskall 确保 CPU 处于 SVC 模式,并且关闭了所有的中断safe_svcmode_maskall 定义在文件 arch/arm/include/asm/assembler.h 中。

②、读处理器 IDID 值保存在 r9 寄存器中。

③、调用函数 __lookup_processor_type 检查当前系统是否支持此 CPU,如果支持就获取 procinfo 信息。 procinfoproc_info_list 类 型 的 结 构 体 , proc_info_list 在文件 arch/arm/include/asm/procinfo.h 中定义

④、调用函数 __create_page_tables 创建页表

⑤、将函数 __mmap_switched 的地址保存到 r13 寄存器中。__mmap_switched 定义在文件 arch/arm/kernel/head-common.S__mmap_switched 最终会调用 start_kernel 函数

⑥、调 用 __enable_mmu 函 数 使 能 MMU__enable_mmu 定 义 在 文 件 arch/arm/kernel/head.S 中。__enable_mmu 最终会通过调用 __turn_mmu_on 来打开 MMU,__turn_mmu_on 最后会执行 r13 里面保存的 __mmap_switched 函数。

2.2 __mmap_switched 函数

__mmap_switched 函数定义在文件 arch/arm/kernel/head-common.S 中,函数代码如下:

/*
 * The following fragment of code is executed with the MMU on in MMU mode,
 * and uses absolute addresses; this is not position independent.
 *
 *  r0  = cp#15 control register
 *  r1  = machine ID
 *  r2  = atags/dtb pointer
 *  r9  = processor ID
 */
	__INIT
__mmap_switched:
	adr	r3, __mmap_switched_data

	ldmia	r3!, {r4, r5, r6, r7}
	cmp	r4, r5				@ Copy data segment if needed
1:	cmpne	r5, r6
	ldrne	fp, [r4], #4
	strne	fp, [r5], #4
	bne	1b

	mov	fp, #0				@ Clear BSS (and zero fp)
1:	cmp	r6, r7
	strcc	fp, [r6],#4
	bcc	1b

 ARM(	ldmia	r3, {r4, r5, r6, r7, sp})
 THUMB(	ldmia	r3, {r4, r5, r6, r7}	)
 THUMB(	ldr	sp, [r3, #16]		)
	str	r9, [r4]			@ Save processor ID
	str	r1, [r5]			@ Save machine type
	str	r2, [r6]			@ Save atags pointer
	cmp	r7, #0
	strne	r0, [r7]			@ Save control register values
	b	start_kernel
ENDPROC(__mmap_switched)

__mmap_switched 函数最终调用 start_kernel 来启动 Linux 内核,start_kernel 函数定义在文件 init/main.c中。

2.3 start_kernel 函数

start_kernel 通过调用众多的子函数来完成 Linux 启动之前的一些初始化工作,由于 start_kernel 函数里面调用的子函数太多,而这些子函数又很复杂,因此我们简单的来看一下一些重要的子函数。精简并添加注释后的 start_kernel 函数内容如下:

asmlinkage __visible void __init start_kernel(void)
{
 char *command_line;
 char *after_dashes;
 lockdep_init(); /* lockdep 是死锁检测模块,此函数会初始化
 * 两个 hash 表。此函数要求尽可能早的执行! 
 */
 set_task_stack_end_magic(&init_task);/* 设置任务栈结束魔术数,
*用于栈溢出检测
*/
 smp_setup_processor_id(); /* 跟 SMP 有关(多核处理器),设置处理器 ID。
 * 有很多资料说 ARM 架构下此函数为空函数,那是因
 * 为他们用的老版本 Linux,而那时候 ARM 还没有多
 * 核处理器。
*/
 debug_objects_early_init(); /* 做一些和 debug 有关的初始化 */
 boot_init_stack_canary(); /* 栈溢出检测初始化 */
 cgroup_init_early(); /* cgroup 初始化,cgroup 用于控制 Linux 系统资源*/
 local_irq_disable(); /* 关闭当前 CPU 中断 */
 early_boot_irqs_disabled = true;
 /*
 * 中断关闭期间做一些重要的操作,然后打开中断
 */
 boot_cpu_init(); /* 跟 CPU 有关的初始化 */
 page_address_init(); /* 页地址相关的初始化 */
 pr_notice("%s", linux_banner);/* 打印 Linux 版本号、编译时间等信息 */
 setup_arch(&command_line); /* 架构相关的初始化,此函数会解析传递进来的
 * ATAGS 或者设备树(DTB)文件。会根据设备树里面
 * 的 model 和 compatible 这两个属性值来查找
 * Linux 是否支持这个单板。此函数也会获取设备树
 * 中 chosen 节点下的 bootargs 属性值来得到命令
 * 行参数,也就是 uboot 中的 bootargs 环境变量的
* 值,获取到的命令行参数会保存到
*command_line 中。
 */
 mm_init_cpumask(&init_mm); /* 看名字,应该是和内存有关的初始化 */
 setup_command_line(command_line); /* 好像是存储命令行参数 */
 setup_nr_cpu_ids(); /* 如果只是 SMP(多核 CPU)的话,此函数用于获取
 * CPU 核心数量,CPU 数量保存在变量
 * nr_cpu_ids 中。
*/
 setup_per_cpu_areas(); /* 在 SMP 系统中有用,设置每个 CPU 的 per-cpu 数据 */
 smp_prepare_boot_cpu(); 
 build_all_zonelists(NULL, NULL); /* 建立系统内存页区(zone)链表 */
 page_alloc_init(); /* 处理用于热插拔 CPU 的页 */
/* 打印命令行信息 */ 
pr_notice("Kernel command line: %s\n", boot_command_line);
 parse_early_param(); /* 解析命令行中的 console 参数 */
 after_dashes = parse_args("Booting kernel",
 static_command_line, __start___param,
 __stop___param - __start___param,
 -1, -1, &unknown_bootoption);
 if (!IS_ERR_OR_NULL(after_dashes))
 parse_args("Setting init args", after_dashes, NULL, 0, -1, -1,
 set_init_arg);
 jump_label_init();
 setup_log_buf(0); /* 设置 log 使用的缓冲区*/
 pidhash_init(); /* 构建 PID 哈希表,Linux 中每个进程都有一个 ID,
 * 这个 ID 叫做 PID。通过构建哈希表可以快速搜索进程
 * 信息结构体。
 */
vfs_caches_init_early(); /* 预先初始化 vfs(虚拟文件系统)的目录项和
* 索引节点缓存
*/
 sort_main_extable(); /* 定义内核异常列表 */
 trap_init(); /* 完成对系统保留中断向量的初始化 */
 mm_init(); /* 内存管理初始化 */
 sched_init(); /* 初始化调度器,主要是初始化一些结构体 */
 preempt_disable(); /* 关闭优先级抢占 */
 if (WARN(!irqs_disabled(), /* 检查中断是否关闭,如果没有的话就关闭中断 */
 "Interrupts were enabled *very* early, fixing it\n"))
 local_irq_disable();
 idr_init_cache(); /* IDR 初始化,IDR 是 Linux 内核的整数管理机
 * 制,也就是将一个整数 ID 与一个指针关联起来。
 */
 rcu_init(); /* 初始化 RCU,RCU 全称为 Read Copy Update(读-拷贝修改) */
 trace_init(); /* 跟踪调试相关初始化 */
 context_tracking_init(); 
 radix_tree_init(); /* 基数树相关数据结构初始化 */
 early_irq_init(); /* 初始中断相关初始化,主要是注册 irq_desc 结构体变
 * 量,因为 Linux 内核使用 irq_desc 来描述一个中断。
 */
 init_IRQ(); /* 中断初始化 */
 tick_init(); /* tick 初始化 */
 rcu_init_nohz(); 
 init_timers(); /* 初始化定时器 */
 hrtimers_init(); /* 初始化高精度定时器 */
 softirq_init(); /* 软中断初始化 */
 timekeeping_init(); 
 time_init(); /* 初始化系统时间 */
 sched_clock_postinit(); 
 perf_event_init();
 profile_init();
 call_function_init();
 WARN(!irqs_disabled(), "Interrupts were enabled early\n");
 early_boot_irqs_disabled = false;
 local_irq_enable(); /* 使能中断 */
 kmem_cache_init_late(); /* slab 初始化,slab 是 Linux 内存分配器 */
 console_init(); /* 初始化控制台,之前 printk 打印的信息都存放
 * 缓冲区中,并没有打印出来。只有调用此函数
 * 初始化控制台以后才能在控制台上打印信息。
 */
 if (panic_later) 
 panic("Too many boot %s vars at `%s'", panic_later,
 panic_param);
 lockdep_info();/* 如果定义了宏 CONFIG_LOCKDEP,那么此函数打印一些信息。*/
 locking_selftest() /* 锁自测 */ 
 ......
 page_ext_init(); 
 debug_objects_mem_init();
 kmemleak_init(); /* kmemleak 初始化,kmemleak 用于检查内存泄漏 */
 setup_per_cpu_pageset(); 
 numa_policy_init();
 if (late_time_init)
 late_time_init();
 sched_clock_init(); 
 calibrate_delay(); /* 测定 BogoMIPS 值,可以通过 BogoMIPS 来判断 CPU 的性能
 * BogoMIPS 设置越大,说明 CPU 性能越好。
 */
 pidmap_init(); /* PID 位图初始化 */
 anon_vma_init(); /* 生成 anon_vma slab 缓存 */ 
## 最后

**自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。**

**深知大多数Java工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助。**

**因此收集整理了一份《2024年嵌入式&物联网开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。**

![img](https://img-blog.csdnimg.cn/img_convert/165ae3cc9bd878e1be4b151a330515ef.png)

![img](https://img-blog.csdnimg.cn/img_convert/a7d5192fd35e8a965ddaf4af87441f56.jpeg)

![img](https://img-blog.csdnimg.cn/img_convert/68cfc4b66073feb830e854bbdfea5572.png)

 ![img](https://img-blog.csdnimg.cn/img_convert/4744f5878bef5bf39f7efaece5507c24.png)

![img](https://img-blog.csdnimg.cn/img_convert/aba748d8506a7c631638a8b75ed41f1d.png)

![img](https://img-blog.csdnimg.cn/img_convert/8ab679ae1b1c96d76db4fe01ddfa015c.png)

![](https://img-blog.csdnimg.cn/img_convert/54103789ca86fa4df63f34bc34c11eea.png)

 

**既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上嵌入式&物联网开发知识点,真正体系化!**

[**如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!**](https://bbs.csdn.net/topics/618654289)

**由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新**!!


715587161392)]

[外链图片转存中...(img-3xFxmtQY-1715587161393)]

 [外链图片转存中...(img-RaRIsGuz-1715587161393)]

[外链图片转存中...(img-2ca8gSIJ-1715587161394)]

[外链图片转存中...(img-FwFzxbuQ-1715587161394)]

[外链图片转存中...(img-RZf2iQLd-1715587161395)]

 

**既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上嵌入式&物联网开发知识点,真正体系化!**

[**如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!**](https://bbs.csdn.net/topics/618654289)

**由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新**!!


  • 20
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
实现了对ARM920T中MMU的启用 地址转换过程 1. 从CP15寄存器C2得到一级页表的基地址 2. 将虚拟地址[31:20]作为页表的索引,得到页表中该虚拟地址的描述符。 3. 判断该描述符是否为段描述符,如为段描述符,将该描述符[31:20]和虚拟地址[19:0]作为偏移量组成一个32位的物理地址进行访问。 4. 如为粗页表描述符,则将该粗页表描述符[31:10]作为二级页表的基地址,并将虚拟地址[19:12]位作为索引得到在二级页表中该虚拟地址的描述符。 判断二级页表符的类型 ① 为极大页描述符表将该描述符[31:16]作为基地和虚拟地址[15:0]作为偏移量得到该虚拟地址的32位物理地址进行访问。 ② 为小页描述符表将描述符[31:12]作为基地和虚拟地址[11:0]作为偏移量得到该虚拟地址的32位物理地址进行访问。 5. 如为细页表描述符,将该组页表描述符[31:12]作为二级页表的基地址,并将虚拟地址[19:10]位作为索引得到在二级页表中该虚拟地址的描述符。 判断二级页表符的类型 ① 为大页描述符表将该描述符[31:16]作为基地和虚拟地址[15:0]作为偏移量得到该虚拟地址的32位物理地址进行访问。 ②为小页描述符表将该描述符[31:12]作为基地和虚拟地址[11:0]作为偏移量得到该虚拟地址的32位物理地址进行访问。 ③为小极页描述符表将描述符[31:10]作为基地和虚拟地址[9:0]作为偏移量得到该虚拟地址的32位物理地址进行访问。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值