以下内容源于朱有鹏嵌入式课程的学习,如有侵权请告知删除。
参考内容
一、前言
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死循环进程(空闲进程)转而去执行有意义的干活的进程。