内核启动流程——C语言阶段的start_kernel函数

以下内容源于朱有鹏嵌入式课程的学习,如有侵权请告知删除。

参考内容

(1)https://blog.51cto.com/u_15333820/3465270 

(2)smp_setup_processor_id-CSDN博客

一、前言

1.1 内容总结

内核的start_kernel 函数位于/init/main.c文件中,主要用来初始化一些内核工作所需的功能模块,比如内存管理、调度系统、异常处理等模块。如果把内核比喻为一台复杂的机器,则该函数的作用,就是将众多零部件组装起来形成这台机器,让它具备工作的基本条件。

1.2 学习方法

1、学习思路

(1)宏观地理解源代码(了解代码执行顺序、作用即可)。

(2)对重点内容要局部、深入地分析。

(3)如果对某个主题感兴趣可以去网上搜索资料学习。

2、学习方法

(1)根据代码的执行顺序来分析源代码。

(2)对照内核启动的打印信息进行分析。

3、学习线路

(1)分析uboot给kernel传参的影响和实现。

(2)分析硬件的初始化与驱动的加载。

(3)分析内核启动后的结局。

4、重点内容

(1)函数setup_arch完成了哪些工作?

(2)内核启动后的稳定状态是怎样的?

二、分析start_kernel函数

smp_setup_processor_id函数

(1)smp,对称多处理器,也即使多核心CPU。

(2)该函数主要作用是获取当前正在执行初始化的处理器ID。

(3)具体介绍见博客smp_setup_processor_id-CSDN博客

(4)该函数的内容如下:

void __init __weak smp_setup_processor_id(void)
{
}

可见该函数是一个空函数。

weak属性可以让编译器在编译的时候忽略函数未定义的错误。如果两个或两个以上全局符号(函数或变量名)名字一样,而其中之一声明为weak symbol(弱符号),则这些全局符号不会引发重定义错误。链接器会忽略弱符号,去使用普通的全局符号来解析所有对这些符号的引用,但当普通的全局符号不可用时,链接器会使用弱符号。

当有函数或变量名可能被用户覆盖时,该函数或变量名可以声明为一个弱符号。

有些架构(比如ARM)拥有自己的smp_setup_processor_id函数,相当于上面所说的普通全局符号,这种情况下链接器就会忽略/init/main.c中的smp_setup_processor_id函数。
 

debug_objects_early_init函数

该函数用于内核的对象调试,依赖于CONFIG_DEBUG_OBJECTS配置项,如下所示:

#ifdef CONFIG_DEBUG_OBJECTS
    extern void debug_objects_early_init(void);
    //忽略其他代码
#else 
    static inline void debug_objects_early_init(void) { }
    //忽略其他代码
#endif

如果定义了CONFIG_DEBUG_OBJECTS,则使用外部定义的函数(位于/lib/debugobjects.c文件中),该函数内容如下:

/*
 * Called during early boot to initialize the hash buckets and link
 * the static object pool objects into the poll list. After this call
 * the object tracker is fully operational.
 */
void __init debug_objects_early_init(void)
{
	int i;

	for (i = 0; i < ODEBUG_HASH_SIZE; i++)
		raw_spin_lock_init(&obj_hash[i].lock);

	for (i = 0; i < ODEBUG_POOL_SIZE; i++)
		hlist_add_head(&obj_static_pool[i].node, &obj_pool);
}

该函数初始化obj_hash、obj_static_pool这2个全局变量,这2个全局变量会在调试的时候用到。 

boot_init_stack_canary函数

函数初始化堆栈保护的canary值,用来防止栈溢出攻击。

lockdep_init函数

锁定依赖,是一个内核调试模块,与处理内核自旋锁死锁问题有关。

cgroup_init_early函数

(1)cgroup,是control group的缩写,内核提供的一种来处理进程组的技术。

(2)这个函数用来初始化cgroup所需要的参数。依赖于CONFIG_CGROUPS这个配置项,如果没有定义这个配置项则为空函数,如果定义了则使用/kernel/cgroup/cgroup.c文件中的函数。

(3)如果要研究cgroup,可以从这个函数入手。

local_irq_disable函数

该函数关闭当前CPU上的所有中断。

因为初始化还没有结束,如果使能中断可能会带来意想不到的后果。所以必须先关闭,等初始化结束后再使能中断。

boot_cpu_init函数

该函数位于/init/main.c文件中,正如注释说明,用于激活第一个处理器。

/*
 *	Activate the first processor.
 */

static void __init boot_cpu_init(void)
{
	int cpu = smp_processor_id();
	/* Mark the boot cpu "present", "online" etc for SMP and UP case */
	set_cpu_online(cpu, true);
	set_cpu_active(cpu, true);
	set_cpu_present(cpu, true);
	set_cpu_possible(cpu, true);
}

page_address_init函数

该函数的实现,依赖于 WANT_PAGE_VIRTUAL、HASHED_PAGE_VIRTUAL这些宏:

#if defined(WANT_PAGE_VIRTUAL)
    #define page_address_init()  do { } while(0)
#endif

#if defined(HASHED_PAGE_VIRTUAL)
    void page_address_init(void);
#endif

#if !defined(HASHED_PAGE_VIRTUAL) && !defined(WANT_PAGE_VIRTUAL)
    #define page_address_init()  do { } while(0)
#endif

如果定义了HASHED_PAGE_VIRTUAL,则使用/mm/highmem.c文件中的函数:

void __init page_address_init(void)
{
	int i;

	INIT_LIST_HEAD(&page_address_pool);
	for (i = 0; i < ARRAY_SIZE(page_address_maps); i++)
		list_add(&page_address_maps[i].list, &page_address_pool);
	for (i = 0; i < ARRAY_SIZE(page_address_htable); i++) {
		INIT_LIST_HEAD(&page_address_htable[i].lh);
		spin_lock_init(&page_address_htable[i].lock);
	}
	spin_lock_init(&pool_lock);
}

printk函数

(1)printk函数的作用

它是内核用来向控制台打印信息的函数,类似于应用层编程中的printf。

内核不能使用标准库函数,因此不能使用printf,其实printk就是内核实现的一个printf。

(2)消息输出的级别

#define	KERN_EMERG	"<0>"	/* system is unusable			*/
#define	KERN_ALERT	"<1>"	/* action must be taken immediately	*/
#define	KERN_CRIT	"<2>"	/* critical conditions			*/
#define	KERN_ERR	"<3>"	/* error conditions			*/
#define	KERN_WARNING	"<4>"	/* warning conditions			*/
#define	KERN_NOTICE	"<5>"	/* normal but significant condition	*/
#define	KERN_INFO	"<6>"	/* informational			*/
#define	KERN_DEBUG	"<7>"	/* debug-level messages			*/

Linux中有非常多的地方调用了printk函数来打印信息,如果所有的printk都能打印出来而不加任何限制,则内核启动过程中将得到海量的输出信息,不利于我们调试。

于是Linux内核给每一个printk添加了一个打印级别。级别定义0~7(注意编程的时候要用相应的宏定义,不要直接用数字)分别代表 8 种重要性级别,0表示最重要,7表示最不重要。使用printk时要根据消息的重要性去设置打印级别。

Linux控制台有一个消息过滤显示机制,控制台只会显示级别比控制台定义的级别高的消息。比如控制台的消息显示级别设置为4,则只有printk中消息级别为0~3(也可能是0~4)的才可以显示。

(3)linux_banner字符数组

linux_banner这个字符数组定义如下:

/* FIXED STRINGS! Don't touch! */
const char linux_banner[] =
	"Linux version " UTS_RELEASE " (" LINUX_COMPILE_BY "@"
	LINUX_COMPILE_HOST ") (" LINUX_COMPILER ") " UTS_VERSION "\n";

我们在编译后的内核目录下使用“ grep -nr "UTS_RELEASE" ./ ”搜索得知:

#define UTS_RELEASE "2.6.35.7"

同理得知: 

#define LINUX_COMPILE_BY "root"
#define LINUX_COMPILE_HOST "ubuntu"
#define LINUX_COMPILER "gcc version 4.4.1 (Sourcery G++ Lite 2009q3-67) "
#define UTS_VERSION "#7 PREEMPT Mon Aug 22 16:50:01 CST 2022"

因此内核启动时会打印下面的信息(后期我重新编辑过本博客内容,UTS_VERSION 不一致):

setup_arch函数

setup_arch函数,主要用来确定当前内核对应的硬件平台(包括CPU与机器码)。

该函数在start_kernel函数中被调用的形式是“setup_arch(&command_line);”,可见command_line是输出型参数(从bootloader中获取有关内容,并保存在command_line这个字符指针数组各元素所指向的空间中)。

setup_arch函数位于/arch/arm/kernel/setup.c文件中,内容如下:

void __init setup_arch(char **cmdline_p)
{
	struct tag *tags = (struct tag *)&init_tags;
	struct machine_desc *mdesc;
	char *from = default_command_line;

	unwind_init();

	setup_processor();//关注一下这个函数
	mdesc = setup_machine(machine_arch_type);//关注一下这个函数
	machine_name = mdesc->name;

	if (mdesc->soft_reboot)
		reboot_setup("s");

	if (__atags_pointer)
		tags = phys_to_virt(__atags_pointer);
	else if (mdesc->boot_params)
		tags = phys_to_virt(mdesc->boot_params);

	/*
	 * If we have the old style parameters, convert them to
	 * a tag list.
	 */
	if (tags->hdr.tag != ATAG_CORE)
		convert_to_tag_list(tags);
	if (tags->hdr.tag != ATAG_CORE)
		tags = (struct tag *)&init_tags;

	if (mdesc->fixup)
		mdesc->fixup(mdesc, tags, &from, &meminfo);

	if (tags->hdr.tag == ATAG_CORE) {
		if (meminfo.nr_banks != 0)
			squash_mem_tags(tags);
		save_atags(tags);
		parse_tags(tags);
	}

	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;

	/* parse_early_param needs a boot_command_line */
	strlcpy(boot_command_line, from, COMMAND_LINE_SIZE);

	/* populate cmd_line too for later use, preserving boot_command_line */
	strlcpy(cmd_line, boot_command_line, COMMAND_LINE_SIZE);
	*cmdline_p = cmd_line;

	parse_early_param();

	paging_init(mdesc);
	request_standard_resources(&meminfo, mdesc);

#ifdef CONFIG_SMP
	smp_init_cpus();
#endif

	cpu_init();
	tcm_init();

	/*
	 * Set up various architecture-specific pointers
	 */
	init_arch_irq = mdesc->init_irq;
	system_timer = mdesc->timer;
	init_machine = mdesc->init_machine;

#ifdef CONFIG_VT
#if defined(CONFIG_VGA_CONSOLE)
	conswitchp = &vga_con;
#elif defined(CONFIG_DUMMY_CONSOLE)
	conswitchp = &dummy_con;
#endif
#endif
	early_trap_init();
}

1.1 setup_processor函数

我们来分析一下setup_arch函数里的setup_processor函数。

(1)该函数调用了head-common.S文件中的lookup_processor_type函数来查找CPU信息:

如何寻找的过程,与下面setup_machine函数中的lookup_machine_type函数类似。

(2)我们可以看一下内核启动通过串口打印的相关信息:

1.2 setup_machine函数

我们继续分析​​​​​​setup_arch函数中的setup_machine函数,内容如下: 

(1)setup_arch函数传给setup_machine函数的参数是机器码编号,即machine_arch_type。

这个 machine_arch_type 定义在/include/generated/mach-types.h文件中的32039-32050行,经过分析后可以确定它的值是2456。

(2)setup_machine函数通过传入的机器码,寻找与这个机器码对应的machine_desc描述符,如果找到则返回这个描述符的指针,否则会执行“while(1);”而挂起。

(3)这个寻找过程是通过调用head-common.S文件中的lookup_machine_type函数来实现的。

下面来分析这个实现过程。

【1】首先,内核把各种CPU架构的信息组织成若干machine_desc结构体的实例,然后设置段属性“.arch.info.init”。这部分的内容见博客内核移植——开发板的软件抽象(struct machine_desc)

在/arch/arm/include/mach/arch.h文件中,有如下内容:

比如在/arch/arm/mach-s5pv210/mach-x210.c文件中,有如下内容:

比如在/arch/arm/mach-s3c2410/mach-qt2410.c文件中,有如下内容:

【2】其次,在进行链接的时候,这些描述符就会被链接在一起。

内核编译后生成的链接脚本位于/arch/arm/kernel/vmlinux.lds.S,其部分截图如下:

【3】最后,__lookup_machine_type函数遍历各个描述符,查看哪个描述符中的机器码与传入的机器码相同。

1.3 对command_line进行简单处理

(1)内核对command_line的处理思路

command_line,是指命令行启动参数。

内核设置了一个默认的 command_line(见下面【1】中的分析)。

uboot也可以通过tag给内核再传递一个command_line(即环境变量bootargs):

x210 # print bootargs
bootargs=console=ttySAC2,115200 root=/dev/mmcblk0p2 rw init=/linuxrc rootfstype=ext3
x210 # 

如果uboot给内核传递command_line成功,则内核会优先使用uboot传递的 command_line;如果 uboot没有给内核传递command_line或者传参失败,则内核会使用默认的command_line。

内核为什么要这样设计?因为希望内核是灵活的,可以通过传参对内核进行配置。

(2)这个处理思路在setup_arch函数中实现,如下所示:

【1】default_command_line是一个全局的字符数组,表示内核默认的命令行参数。

在/arch/arm/kernel/setup.c文件中有如下定义:

static char default_command_line[COMMAND_LINE_SIZE] __initdata = CONFIG_CMDLINE;

在内核配置后生成的/.config文件中有如下定义:

 CONFIG_CMDLINE="console=ttySAC2,115200"

 因此内核默认的命令行参数是"console=ttySAC2,115200"。

【2】我们可以看一下内核启动时打印的相关信息:

setup_command_line函数

该函数将command line保存起来,以备将来使用。

/*
 * We need to store the untouched command line for future reference.
 * We also need to store the touched command line since the parameter
 * parsing is performed in place, and we should allow a component to
 * store reference of name/value for future reference.
 */
static void __init setup_command_line(char *command_line)
{
	saved_command_line = alloc_bootmem(strlen (boot_command_line)+1);
	static_command_line = alloc_bootmem(strlen (command_line)+1);
	strcpy (saved_command_line, boot_command_line);
	strcpy (static_command_line, command_line);
}

setup_nr_cpu_ids

setup_per_cpu_areas

build_all_zonelists

page_alloc_init

parse_early_param函数

解析需要早点处理的启动参数 

parse_args函数

对传入内核参数进行解释,如果不能识别的命令就调用最后参数的函数,在kernel/params.c中实现 

(1)这两个函数用来解析cmdline传参和其他传参。比如将cmdline解析,得到一个字符串数组,这个数组中依次存放了一个个设置项目信息。

(2)注意,这两个函数只是进行解析,没有去处理。换言之,这两个函数只是把长字符串解析成短字符串,最多和内核里控制这个相应功能的变量挂钩了,但是并没有去执行。真正执行的代码在各自模块初始化的代码部分。

vfs_caches_init_early函数

sort_main_extable

trap_init

设置异常向量表。

mm_init函数

进行内存管理模块初始化。

ftrace_init

跟踪调试机制初始化,初始化内核跟踪模块。

ftrace的作用是帮助开发人员了解Linux内核的运行时行为,以便于进行故障调试或者性能分析。

early_trace_init

使能trace_printk。

sched_init

初始化进程调度器数据结构并创建运行队列,在kernel/sched/core.c中。

即进行内核调度系统初始化。

preempt_disable

禁止进程内核抢占(早期启动时期,调度是极其脆弱的,需要禁止抢占)。

radix_tree_init

初始化radix树算法初始化,在lib/radix_tree.c中实现。

workqueue_init_early

工作队列早期初始化,在kernel/workqueue.c文件中实现。

rcu_init

trace_init

跟踪事件初始化。

early_irq_init

早期外部中断描述初始化,在kernel/irq/irqdesc.c中有实现。

init_IRQ

架构相关中断初始化,在arch/arm/kernel/irq.c中实现。

tick_init

初始化时钟滴答控制器

init_timers

初始化引导CPU的时钟相关的数据结构,在kernel/timer.c中实现。

hrtimers_init

初始化高精度的定时器,在kernel/hrtimer.c中实现。

softirq_init

初始化软件软件中断,在kernel/softirq.c中实现。

timekeeping_init

初始化系统时钟计时,在kernel/time/timekeeping.c中实现。

time_init

初始化系统时钟,在arch/arm/kernel/time.c中实现。

sched_clock_postinit

对每个CPU进行系统进程调度时钟初始化。

printk_safe_init

位于文件kernel/printk/printk_safe.c

profile_init()

profile初始化。profile是内核诊断工具,在kernel/profile.c中有实现。

call_function_init

在include/linux/amp.h中为空, 在kernel/smp.c中有实现。

kmem_cache_init_late

slab分配器后期初始化,在mm/slob.c、slub.c、slab.c中有实现。

内核启动时使用临时内存分配器bootmem,之后由slab接管。kmem_cache_init_late()初始化了slab分配器(内核高速缓存分配器),这意味着bootmem的结束,同时内核的内存管理系统正式开始工作。bootmem的分配必须先于kmem_cache_init_late()。

console_init

控制台初始化。从这个函数之后就可以输出内容到控制台了。在此之前的输出保存在输出缓冲区中,这个函数被调用之后就马上把之前的内容输出出来。

lockdep_info

如果定义了CONFIG_LOCKDEP宏,那么就打印锁依赖信息,进行自我检测,否则什么也不做 。在include/linux/lockdep.h中为空,在kernel/lockdep.c中有实现。

locking_selftest

mem_encrypt_init

page_ext_init

memleak_init

内存泄漏检测机制的初始化。

在include/linux/kmemleak.h中为空,在mm/kmemleak.c中实现。

debug_objects_mem_init

创建调试对象内存缓存。

在include/linux/debugobjects.h为空,在lib/debugobjects.c中实现

setup_per_cpu_pageset

numa_policy_init

acpi_early_init

calibrate_delay

pid_idr_init

anon_vma_init

thread_stack_cache_init

cred_init

fork_init

proc_caches_init

buffer_init

key_init

security_init

dbg_late_init

vfs_caches_init

pagecache_init

signals_init

proc_root_init

cpuset_init

cgroup_init

taskstats_init_early

delayacct_init

check_bugs

sfi_init_late

rest_init函数

该函数之前,内核的组装基本已经完成,剩下的工作放在 rest_init 函数中完成。

rest_init函数内容如下:

1.1 kernel_init函数(进程1、init进程

rest_init函数通过调用kernel_thread函数,启动了 kernel_init 线程。

1.2 kthreadd函数(进程2、内核的守护进程

rest_init函数通过调用kernel_thread函数,启动了 kthreadd 线程。 

1.3 schedule函数

rest_init函数调用schedule函数开启内核的调度系统,从此Linux系统开始运转起来。


1.4 cpu_idle函数(进程0、idle进程、空闲进程)

rest_init函数最终调用cpu_idle函数,该函数内部是一个死循环。

调度系统负责考评系统中所有的进程,只要有哪个需要被运行,调度系统就会终止cpu_idle死循环进程(空闲进程)转而去执行有意义的干活的进程。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

天糊土

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值