linux内核启动流程

start_kernel

start_kernel是所有 Linux 平台进入系统内核初始化后的入口函数,它主要完成剩余的与 硬件平台相关的初始化工作,在进行一系列与内核相关的初始化后,调用第一个用户进程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
asmlinkage __visible void __init __no_sanitize_address start_kernel(void)
{
char *command_line;
char *after_dashes;

set_task_stack_end_magic(&init_task); // 设置初始任务的栈结束魔术字,用于栈溢出检测
smp_setup_processor_id(); // 设置处理器ID(SMP系统)
debug_objects_early_init(); // 调试对象早期初始化
cgroup_init_early(); // 控制组(cgroup)早期初始化

local_irq_disable(); // 禁用本地中断
early_boot_irqs_disabled = true; // 标记早期启动阶段中断被禁用

/*
* Interrupts are still disabled. Do necessary setups, then
* enable them.
*/
boot_cpu_init(); // 启动CPU初始化
page_address_init(); // 页面地址初始化
pr_notice("%s", linux_banner); // 打印Linux版本信息
early_security_init(); // 安全子系统早期初始化

/*架构相关初始化*/
setup_arch(&command_line); // 架构特定设置(非常重要!)
setup_boot_config(command_line); // 启动配置设置
setup_command_line(command_line); // 保存命令行参数
setup_nr_cpu_ids(); // 设置CPU数量
setup_per_cpu_areas(); // 设置每CPU区域
smp_prepare_boot_cpu(); // 准备启动CPU(架构特定钩子)
boot_cpu_hotplug_init(); // CPU热插拔初始化

/*内存管理初始化*/
build_all_zonelists(NULL); // 构建所有内存区域的区域列表
page_alloc_init(); // 页面分配器初始化

pr_notice("Kernel command line: %s\n", saved_command_line); // 打印内核命令行
/* parameters may set static keys */
jump_label_init(); // 跳转标签初始化(用于静态键)
parse_early_param(); // 解析早期参数
after_dashes = parse_args("Booting kernel", // 解析内核参数
static_command_line, __start___param,
__stop___param - __start___param,
-1, -1, NULL, &unknown_bootoption);
if (!IS_ERR_OR_NULL(after_dashes))
parse_args("Setting init args", after_dashes, NULL, 0, -1, -1,
NULL, set_init_arg);
if (extra_init_args)
parse_args("Setting extra init args", extra_init_args,
NULL, 0, -1, -1, NULL, set_init_arg);

/*
* These use large bootmem allocations and must precede
* kmem_cache_init()
*/
/*核心子系统初始化*/
setup_log_buf(0); // 设置日志缓冲区
vfs_caches_init_early(); // 虚拟文件系统早期缓存初始化
sort_main_extable(); // 排序异常表
trap_init(); // 陷阱/异常处理初始化
mm_init(); // 内存管理初始化

ftrace_init(); // 函数跟踪初始化
early_trace_init(); // 早期跟踪初始化

sched_init(); // 调度器初始化(关键!)
preempt_disable(); // 禁用抢占


if (WARN(!irqs_disabled(),
"Interrupts were enabled *very* early, fixing it\n"))
local_irq_disable();
radix_tree_init();

/*
* Set up housekeeping before setting up workqueues to allow the unbound
* workqueue to take non-housekeeping into account.
*/
housekeeping_init();

/*
* Allow workqueue creation and work item queueing/cancelling
* early. Work item execution depends on kthreads and starts after
* workqueue_init().
*/
workqueue_init_early();

rcu_init();

/* Trace events are available after this */
trace_init();

if (initcall_debug)
initcall_debug_enable();

context_tracking_init(); // 上下文跟踪初始化
early_irq_init(); // 早期中断初始化
init_IRQ(); // 中断控制器初始化
tick_init(); // 时钟滴答初始化
rcu_init_nohz(); // RCU无滴答初始化
init_timers(); // 定时器初始化
hrtimers_init(); // 高分辨率定时器初始化
softirq_init(); // 软中断初始化
timekeeping_init(); // 时间保持初始化

/*
* For best initial stack canary entropy, prepare it after:
* - setup_arch() for any UEFI RNG entropy and boot cmdline access
* - timekeeping_init() for ktime entropy used in rand_initialize()
* - rand_initialize() to get any arch-specific entropy like RDRAND
* - add_latent_entropy() to get any latent entropy
* - adding command line entropy
*/
rand_initialize();
add_latent_entropy();
add_device_randomness(command_line, strlen(command_line));
boot_init_stack_canary();

time_init();
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(); // 延迟的kmem缓存初始化

/*
* HACK ALERT! This is early. We're enabling the console before
* we've done PCI setups etc, and console_init() must be aware of
* this. But we do want output early, in case something goes wrong.
*/
console_init(); // 控制台初始化(现在可以输出了!)
if (panic_later)
panic("Too many boot %s vars at `%s'", panic_later,
panic_param);

lockdep_init();

/*
* Need to run this when irqs are enabled, because it wants
* to self-test [hard/soft]-irqs on/off lock inversion bugs
* too:
*/
locking_selftest();

/*
* This needs to be called before any devices perform DMA
* operations that might use the SWIOTLB bounce buffers. It will
* mark the bounce buffers as decrypted so that their usage will
* not cause "plain-text" data to be decrypted when accessed.
*/
mem_encrypt_init();

#ifdef CONFIG_BLK_DEV_INITRD
if (initrd_start && !initrd_below_start_ok &&
page_to_pfn(virt_to_page((void *)initrd_start)) < min_low_pfn) {
pr_crit("initrd overwritten (0x%08lx < 0x%08lx) - disabling it.\n",
page_to_pfn(virt_to_page((void *)initrd_start)),
min_low_pfn);
initrd_start = 0;
}
#endif
setup_per_cpu_pageset();
numa_policy_init();
acpi_early_init();
if (late_time_init)
late_time_init();
sched_clock_init();
calibrate_delay();
pid_idr_init();
anon_vma_init();
#ifdef CONFIG_X86
if (efi_enabled(EFI_RUNTIME_SERVICES))
efi_enter_virtual_mode();
#endif
thread_stack_cache_init(); // 线程栈缓存初始化
cred_init(); // 凭证初始化
fork_init(); // 进程创建初始化
proc_caches_init(); // proc缓存初始化
uts_ns_init(); // UTS命名空间初始化
buffer_init(); // 缓冲区缓存初始化
key_init(); // 密钥初始化
security_init(); // 安全子系统初始化
dbg_late_init(); // 延迟调试初始化
/*文件系统和进程间通信*/
vfs_caches_init(); // 虚拟文件系统缓存初始化
pagecache_init(); // 页面缓存初始化
signals_init(); // 信号初始化
seq_file_init(); // 序列文件初始化
proc_root_init(); // proc文件系统根初始化
nsfs_init(); // 命名空间文件系统初始化
cpuset_init(); // cpuset初始化
cgroup_init(); // cgroup初始化
taskstats_init_early(); // 任务统计早期初始化
delayacct_init(); // 延迟统计初始化

poking_init();
check_bugs();

acpi_subsystem_init();
arch_post_acpi_subsys_init();
sfi_init_late();
kcsan_init();

/* Do the rest non-__init'ed, we're now alive */
arch_call_rest_init(); // 调用架构特定的rest_init()
prevent_tail_call_optimization(); // 防止尾调用优化
}

start_kernel函数主要调用了以下子函数来完成初始化:

  • setup_arch(&command_line): 架构特定的第二次初始化,解析设备树(DTB)或ATAGS(传递自Bootloader的参数),进行内存映射等。
  • trap_init(): 初始化异常向量表中断处理函数的入口。
  • mm_init(): 内存管理子系统初始化,初始化伙伴系统(Buddy System)等 slab allocator 的基础。
  • sched_init(): 调度器初始化,初始化系统进程调度器。
  • early_irq_init()init_IRQ(): 中断处理机制初始化
  • time_init(): 系统时钟初始化,读取实时时钟(RTC),初始化定时器中断。
  • console_init(): 控制台初始化。在这之前,内核通过printk输出的信息可能只是保存在缓冲区中。此后,信息才能显示在控制台上。
  • arch_call_rest_init(): 这是start_kernel()的最后一步,它会调用rest_init(),后者将创建第一个用户空间进程(init)并完成启动过程

创建第一个进程

start_kernel在最后调用rest_init(),它负责结束内核启动并“孵化”出用户空间。

工作内容:

  • 通过kernel_thread创建内核线程 kernel_init。这个线程就是后续的用户空间init进程(PID 1) 的雏形。
  • 通过kernel_thread创建内核线程 kthreadd(PID 2),它负责管理和调度所有其他的内核线程。
  • 调用schedule()开启进程调度
  • 将当前任务(0号进程,即idle进程)标记为可调度,并调用cpu_idle()进入空闲循环。当没有其他任务可运行时,CPU就执行idle进程。

initramfs的处理与根文件系统挂载

kernel_init线程首先会尝试处理initramfs

  • 为什么需要initramfs?
    内核可能不包含访问真实根文件系统所在磁盘所需的驱动程序(例如SCSI、RAID、LVM、加密设备的驱动)。initramfs是一个临时的、放在内存中的根文件系统,它包含了这些驱动和工具。在内核无法直接访问根设备时,提供一个临时的环境来加载必要驱动,从而挂载真正的根文件系统。
  • 工作内容:
    1. 内核解压并加载initramfs到内存的一个临时文件系统中。
    2. kernel_init线程执行initramfs中的/init脚本(这是一个用户空间程序!)。
    3. /init脚本负责加载必要的硬件驱动模块(如磁盘控制器、文件系统驱动)。
    4. 驱动加载后,/init脚本挂载真正的根文件系统(例如/dev/sda1)。
    5. 最后,/init脚本通过pivot_rootchroot系统调用,将根目录切换到新挂载的真实根文件系统上。
    6. /init程序退出,kernel_init线程继续执行。

用户空间初始化

切换到真正的根文件系统后,kernel_init线程会尝试执行用户空间的第一个程序。

  • kernel_init会按顺序尝试执行以下程序之一:
    • /sbin/init (最常见)
    • /etc/init
    • /bin/init
    • /bin/sh
  • 通常,/sbin/init是一个指向现代init系统(如systemdSysV init)的符号链接。
  • init进程(PID 1) 被启动,它成为所有用户进程的父进程。
  • init系统根据其配置文件(如systemd的/etc/systemd/system/目录或SysV init的/etc/inittab/etc/rc.d/脚本)来:
    • 初始化主机名、挂载文件系统(/proc, /sys, /dev)、设置内核参数。
    • 启动系统服务(如网络、日志、定时任务)。
    • 启动登录管理器(如图形界的GDM、LightDM)或文本界的getty进程。
    • getty进程在终端上显示login:提示符,等待用户登录。

linux移植工作

  1. 添加开发板默认配置文件

    将arch/arm/configs 目录下的imx_v7_mfg_defconfig 重新复制一份, 命名为imx_alientek_emmc_defconfig,打开imx_alientek_emmc_defconfig 文件,找到“CONFIG_ARCH_MULTI_V6=y”这一行,将其屏蔽掉(因为I.MX6ULL 是ARMV7 架构的,因此要屏蔽掉V6 相关选项,否则后面做驱动实验的时候可能会遇到驱动模块无法加载的情况。)

    1
    2
    3
    4
    5
    6
    #CPU Core family selection
    CONFIG_ARCH_MULTI_V6=y#屏蔽
    CONFIG_ARCH_MULTI_V7=y
    CONFIG_ARCH_MULTI_V6_V7=y
    CONFIG_ARCH_MULTI_CPU_AUTO is not set
    CONFIG_ARCH_VIRTisnot set

    以后就可以使用如下命令来配置正点原子EMMC版开发板对应的Linux 内核了

    1
    make imx_alientek_emmc_defconfig
  2. 添加开发板对应的设备树文件

    添加适合正点原子EMMC 版开发板的设备树文件,进入目录arch/arm/boot/dts 中,复制一
    份imx6ull-14x14-evk.dts,然后将其重命名为imx6ull-alientek-emmc.dts

  3. 编译测试

    1
    2
    3
    4
    5
    #!/bin/sh
    make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- distclean
    make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihfimx_alientek_emmc_defconfig
    make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- menuconfig
    make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- all -j16

    执行shell 脚本imx6ull_alientek_emmc.sh 编译Linux 内核

    编译完成以后就会在目录arch/arm/boot 下生成zImage 镜像文件。在arch/arm/boot/dts 目录下生成imx6ull-alientek-emmc.dtb 文件

linux内核裁剪

  • 裁剪原因: IMX6ULL这款资源有限的ARM处理器制作一个轻量、高效且稳定可靠的定制化系统,移除不需要的模块可以减小内核体积、降低内存占用、提高启动速度,并减少潜在的安全漏洞。”

  • 主要工具:我使用标准的 make menuconfig 基于图形界面进行配置,因为它相比 make xconfig 更轻量,相比 make config 更直观。

  • 基本方法:首先获取芯片原厂(NXP)提供的默认配置文件(通常是 imx_v7_defconfig),以此为基础进行修改,而不是从零开始配置。这样可以最大程度保证硬件基本功能的正常。

    1
    2
    3
    4
    5
    6
    # 获取默认配置
    make imx_v7_defconfig
    # 进入交互配置菜单
    make menuconfig
    # 编译内核
    make -j4
  • 具体裁剪内容及原因:

    • 处理器类型与平台支持:移除所有其他架构的CPU支持(如x86, PowerPC, MIPS等),以及NXP i.MX系列中其他型号的芯片支持(如i.MX7, i.MX8等)。
    • 设备驱动: 这是裁剪的大头。驱动代码量非常大,针对性保留可以极大缩小内核尺寸。
      • 图形驱动:移除所有其他GPU(如NVIDIA, AMD)和显示器驱动。
      • 输入设备:移除了游戏手柄、触摸板等驱动(我们只保留了触摸屏驱动evdev)。
      • 声卡驱动:整个系统没有音频功能,所以全部移除。
    • 文件系统:
  • 经过上述裁剪,最终的内核镜像(zImage)大小从原始的7MB减少到了5MB,系统启动时间和内存占用也有了明显的优化。

  • 验证

    • 基本功能测试:确保系统能正常启动、挂载根文件系统。
    • 硬件功能测试:逐一测试所有需要的外设功能是否正常:I2C读写传感器、SPI通信、PWM控制舵机、GPIO控制风扇/水泵、以太网通信、LCD显示和触摸。