1、阅读 Linux 内核源码
学习 Linux 有两种路线:
1)按照 Linux 启动流程,梳理每个子系统。
2)把 Linux 所有用到的子系统学会,再组合起来。
博主选择第一种方式,可以快速上手,知道自己在学什么东西,在什么阶段起作用。
阅读 Linux 和 Android 源码:
https://elixir.bootlin.com/linux/latest/source
http://aospxref.com/
2、Makefile 与 Kconfig
1)Makefile
- Makefile 是一种被广泛用于管理源代码的工具,特别是针对程序的编译和构建。它包含了一系列规则,指定了编译器如何编译源文件、链接器如何链接目标文件,以及如何清理生成的文件等操作。
- Makefile 的作用:通过 Makefile,开发人员可以指定项目中源文件的依赖关系,使得只有受影响的文件被编译,而不是每次都编译整个项目,节省时间和资源。
- Makefile 结构:典型的 Makefile 包含了变量定义、目标规则、依赖关系和命令等内容。
2)Kconfig
- Kconfig 是 Linux 内核用于配置内核选项的工具,允许用户在编译内核时选择不同的配置选项以定制内核的功能。
- Kconfig 文件:Kconfig 文件包含了内核配置选项的描述,格式为类似于菜单的层次结构,用户可以通过命令行或图形界面交互式地选择选项,以配置内核的特性和行为。
- Kconfig 的作用:Kconfig 允许用户对内核进行高度定制,以适应不同的硬件平台、应用需求或性能要求,而无需重新编译整个内核。
3)总结
- Makefile 是用于管理源代码编译和构建的工具,利用规则来指导建立整个项目。
- Kconfig 是 Linux 内核的配置工具,用于选择编译内核时的不同配置选项,定制内核的功能和行为。
3、内核裁剪与内核移植
1)内核裁剪
make menuconfig 命令在 Linux 内核源码根目录执行,执行后效果如图:
- “方向按键”中的“左右”可以选择你需要的操作。“”表示进入选择的配置界面,“”表示返回,“< Help >”可以阅读帮助文档。
- 输入“/”,可以进入搜索界面。
make menuconfig 是 Linux 内核构建过程中的一个命令,用于交互式地配置内核选项。通过运行 make menuconfig 命令,用户可以在终端中打开一个基于文本界面的菜单系统,以便于选择和配置 Linux 内核的各种功能选项、驱动程序和特性。
具体来说,make menuconfig 命令的作用包括以下几点:
- 配置内核选项:通过 make menuconfig,用户可以浏览各种内核选项,例如驱动程序、文件系统支持、网络配置等,并可以根据需求进行启用、禁用、选择或设置。
- 交互式选择:使用键盘进行上下左右移动,空格键选择选项,以及对选项进行开启或关闭操作。
- 依赖关系:make menuconfig 会显示选项之间的依赖关系,使得用户可以清晰地了解选择一个选项可能会影响到哪些其他选项。
- 保存配置:按 ESC 可以退出这个界面,会提示你是否要保存。配置的修改可以保存并生成 .config 文件,该文件包含了用户的配置选择,可用于后续编译内核。
- 定制内核:通过调整不同的配置选项,用户可以定制适合特定需求的内核,例如精简内核以提高性能,或者增加特定的功能支持。
总的来说,make menuconfig 提供了一种方便、交互式的方式来配置 Linux 内核,使得用户可以根据自己的需求定制内核的功能和特性。
2)内核移植
4、Linux 启动流程详解
建议大家深入学习 Linux kernel 某个子系统之前,先学习 Linux kernel 启动流程,这样能有一个框架,你学的子系统就不是孤立存在于你的知识系统中,你知道它是在哪个时刻被调用的。
从启动引导程序 bootloader(uboot)跳转到 Linux kernel 后,Linux 内核开始启动, 我们先看一下 Linux 内核启动入口。
linux4.14/arch/arm/kernel/vmlinux.lds.S
这里可以看到链接时候 Linux 入口是 stext 段,这里是 uboot 跳转过来的第一段 Linux kernel 代码:
OUTPUT_ARCH(arm)
ENTRY(stext)
1)Linux 入口地址
我们先看一下入口地址的确定,同一文件中找到 SECTIONS
SECTIONS
{
/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.*)
}
. = PAGE_OFFSET + TEXT_OFFSET;
.head.text : {
_text = .;
HEAD_TEXT
}
这个 SECTIONS 比较长,只放一部分。在这里倒数第五行有个比较重要的东西:
. = PAGE_OFFSET + TEXT_OFFSET;
这一句表示了 Linux kernel 真正的启动地址。
PAGE_OFFSET 是 Linux kernel 空间的虚拟起始地址,定义在:
linux4.14/arch/arm64/include/asm/memory.h
#define VA_BITS (CONFIG_ARM64_VA_BITS)
#define VA_START (UL(0xffffffffffffffff) - \
(UL(1) << VA_BITS) + 1)
#define PAGE_OFFSET (UL(0xffffffffffffffff) - \
(UL(1) << (VA_BITS - 1)) + 1)
#define KIMAGE_VADDR (MODULES_END)
#define MODULES_END (MODULES_VADDR + MODULES_VSIZE)
#define MODULES_VADDR (VA_START + KASAN_SHADOW_SIZE)
#define MODULES_VSIZE (SZ_128M)
#define VMEMMAP_START (PAGE_OFFSET - VMEMMAP_SIZE)
#define PCI_IO_END (VMEMMAP_START - SZ_2M)
#define PCI_IO_START (PCI_IO_END - PCI_IO_SIZE)
#define FIXADDR_TOP (PCI_IO_START - SZ_2M)
这里的地址都很重要,很多地方会用到。当然,这里的地址可能会随着 Linux kernel 版本的不同和硬件的不同,会变化。这里没有一个具体的数,因为 VA_BITS 中的数字是可选的,大家可以根据自己的平台算一下。 VA_BITS 是你的系统真正使用的位数,如果是 32 位系统就是 32,如果是 64 位系统可能是 39 位或者其他。
TEXT_OFFSET 定义在 linux4.14/arch/arm/Makefile
中:
textofs-y := 0x00008000
The byte offset of the kernel image in RAM from the start of RAM.
TEXT_OFFSET := $(textofs-y)
这个值一般是 0x00008000 ,算出 PAGE_OFFSET 后加上这个值就是 Linux 内核的起始地址。
修改这个偏移量就可以使 Linux 内核拷贝到不同的地址,自己修改注意内存对齐。
2)stext 段
从上面的 ENTRY(stext)可以知道,一开始是运行 stext 段,这个段内的代码是 start_kernel 函数前汇编环境的初始化。
linux4.14/arch/arm64/kernel/head.S
ENTRY(stext)
bl preserve_boot_args
bl el2_setup // Drop to EL1, w0=cpu_boot_mode
adrp x23, __PHYS_OFFSET
and x23, x23, MIN_KIMG_ALIGN - 1 // KASLR offset, defaults to 0
bl set_cpu_boot_mode_flag
bl __create_page_tables
bl __cpu_setup // initialise processor
b __primary_switch
ENDPROC(stext)
- preserve_boot_args 保存 uboot 传递过来的参数。
- el2_setup 是设置 Linux 启动模式是 EL2。Linux 有 EL0、EL1、EL2、EL3 四种异常启动模式,这里设置一开始是 EL2,EL2 支持虚拟内存技术,然后注释说明后面又退回 EL1,在 EL1 启动 kernel。EL3 一般是只在安全模式使用。(ARM64情况下)
- 4、5 行:这两行设定了 offset,是一种 KASLR 技术,有了这个技术才能使得 Linux kernel 被拷贝到不同的物理地址。
- set_cpu_boot_mode_flag 保存上面 cpu 的启动模式。
- __create_page_tables 创建页表。
- __cpu_setup 初始化 CPU,这里主要是初始化和 MMU 内存相关的 CPU 部分。
- __primary_switch 这里会进行跳转。
在同一个文件中,会跳转到这里:
__primary_switch:
#ifdef CONFIG_RANDOMIZE_BASE
mov x19, x0 // preserve new SCTLR_EL1 value
mrs x20, sctlr_el1 // preserve old SCTLR_EL1 value
#endif
bl __enable_mmu
#ifdef CONFIG_RELOCATABLE
bl __relocate_kernel
#ifdef CONFIG_RANDOMIZE_BASE
ldr x8, =__primary_switched
adrp x0, __PHYS_OFFSET
blr x8
开启了 MMU。然后最重要的是跳转到 __primary_switched 函数。先把 __primary_switched 地址放到 x8 寄存器中,再跳转到 x8,也就是跳转到 __primary_switched。
__primary_switched:
adrp x4, init_thread_union
add sp, x4, #THREAD_SIZE
adr_l x5, init_task
msr sp_el0, x5 // Save thread_info
adr_l x8, vectors // load VBAR_EL1 with virtual
msr vbar_el1, x8 // vector table address
isb
stp xzr, x30, [sp, #-16]!
mov x29, sp
str_l x21, fdt_pointer, __x5 // Save FDT pointer
ldr_l x4, kimage_vaddr // Save the offset between
sub x4, x4, x0 // the kernel virtual and
str_l x4, kimage_voffset, x5 // physical mappings
// Clear BSS
adr_l x0, __bss_start
mov x1, xzr
adr_l x2, __bss_stop
sub x2, x2, x0
bl __pi_memset
dsb ishst // Make zero page visible to PTW
#ifdef CONFIG_KASAN
bl kasan_early_init
#endif
#ifdef CONFIG_RANDOMIZE_BASE
tst x23, ~(MIN_KIMG_ALIGN - 1) // already running randomized?
b.ne 0f
mov x0, x21 // pass FDT address in x0
bl kaslr_early_init // parse FDT for KASLR options
cbz x0, 0f // KASLR disabled? just proceed
orr x23, x23, x0 // record KASLR offset
ldp x29, x30, [sp], #16 // we must enable KASLR, return
ret // to __primary_switch()
0:
#endif
add sp, sp, #16
mov x29, #0
mov x30, #0
b start_kernel
ENDPROC(__primary_switched)
- 第一段: 初始化了 init 进程的内存信息,开辟了内存空间。
- 第二段:设置了中断向量表。
- 第三段: 保存了 FDT,也就是 flat device tree ,设备树。
- 第四段:清除了BSS 段,我们知道一般是内存四区:堆区、栈区、全局区、代码区。其中全局区可以再分为 data 段和 BSS 段,BSS 段存储了未初始化的变量,这里将BSS段进行清零操作,否则内存中的值是不确定的,这是一个传统操作。
- 最后:跳转到了我们熟悉的 start_kernel
3)start_kernel 函数
linux4.14/init/main.c,start_kernel 函数,从汇编环境跳转到了 C 环境。
kernel 4.14 中 start_kernel 函数一共调用了 86 个函数进行初始化 ,如下:
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();/*跟 SMP 有关(多核处理器),设置处理器 ID*/
debug_objects_early_init();/* 做一些和 debug 有关的初始化 */
init_vmlinux_build_id();
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 版本号、编译时间等信息 */
early_security_init();
/* 系统架构相关的初始化,此函数会解析传递进来的
* ATAGS 或者设备树(DTB)文件。会根据设备树里面
* 的 model 和 compatible 这两个属性值来查找
* Linux 是否支持这个单板。此函数也会获取设备树
* 中 chosen 节点下的 bootargs 属性值来得到命令
* 行参数,也就是 uboot 中的 bootargs 环境变量的
* 值,获取到的命令行参数会保存到 command_line 中
*/
setup_arch(&command_line);
setup_boot_config();
setup_command_line(command_line);/* 存储命令行参数 */
/* 如果只是 SMP(多核 CPU)的话,此函数用于获取
* CPU 核心数量,CPU 数量保存在变量 nr_cpu_ids 中。
*/
setup_nr_cpu_ids();
setup_per_cpu_areas();/* 在 SMP 系统中有用,设置每个 CPU 的 per-cpu 数据 */
smp_prepare_boot_cpu(); /* arch-specific boot-cpu hooks */
boot_cpu_hotplug_init();
build_all_zonelists(NULL);/* 建立系统内存页区(zone)链表 */
page_alloc_init();/* 处理用于热插拔 CPU 的页 */
/* 打印命令行信息 */
pr_notice("Kernel command line: %s\n", saved_command_line);
/* parameters may set static keys */
jump_label_init();
parse_early_param();/* 解析命令行中的 console 参数 */
after_dashes = parse_args("Booting kernel",
static_command_line, __start___param,
__stop___param - __start___param,
-1, -1, NULL, &unknown_bootoption);
print_unknown_bootoptions();
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);
random_init_early(command_line);
setup_log_buf(0);/* 设置 log 使用的缓冲区*/
vfs_caches_init_early(); /* 预先初始化 vfs(虚拟文件系统)的目录项和索引节点缓存*/
sort_main_extable();/* 定义内核异常列表 */
trap_init();/* 完成对系统保留中断向量的初始化 */
mm_init();/* 内存管理初始化 */
ftrace_init();
/* trace_printk can be enabled here */
early_trace_init();
sched_init();/* 初始化调度器,主要是初始化一些结构体 */
if (WARN(!irqs_disabled(),"Interrupts were enabled *very* early, fixing it\n"))
local_irq_disable();/* 检查中断是否关闭,如果没有的话就关闭中断 */
radix_tree_init();/* 基数树相关数据结构初始化 */
maple_tree_init();
housekeeping_init();
workqueue_init_early();
rcu_init();/* 初始化 RCU,RCU 全称为 Read Copy Update(读-拷贝修改) */
/* Trace events are available after this */
trace_init();/* 跟踪调试相关初始化 */
if (initcall_debug)
initcall_debug_enable();
context_tracking_init();
/* 初始中断相关初始化,主要是注册 irq_desc 结构体变
* 量,因为 Linux 内核使用 irq_desc 来描述一个中断。
*/
early_irq_init();
init_IRQ();/* 中断初始化 */
tick_init();/* tick 初始化 */
rcu_init_nohz();
init_timers();/* 初始化定时器 */
srcu_init();
hrtimers_init();/* 初始化高精度定时器 */
softirq_init();/* 软中断初始化 */
timekeeping_init();
time_init();/* 初始化系统时间 */
random_init();
kfence_init();
boot_init_stack_canary();
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 内存分配器 */
/* 初始化控制台,之前 printk 打印的信息都存放
* 缓冲区中,并没有打印出来。只有调用此函数
* 初始化控制台以后才能在控制台上打印信息。
*/
console_init();
if (panic_later)
panic("Too many boot %s vars at `%s'", panic_later,
panic_param);
lockdep_init();
locking_selftest();/* 锁自测 */
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();
/* 测定 BogoMIPS 值,可以通过 BogoMIPS 来判断 CPU 的性能
* BogoMIPS 设置越大,说明 CPU 性能越好。
*/
calibrate_delay();
pid_idr_init();
anon_vma_init();/* 生成 anon_vma slab 缓存 */
#ifdef CONFIG_X86
if (efi_enabled(EFI_RUNTIME_SERVICES))
efi_enter_virtual_mode();
#endif
thread_stack_cache_init();
cred_init();/* 为对象的每个用于赋予资格(凭证) */
fork_init();/* 初始化一些结构体以使用 fork 函数 */
proc_caches_init();/* 给各种资源管理结构分配缓存 */
uts_ns_init();
key_init();/* 初始化密钥 */
security_init();/* 安全相关初始化 */
dbg_late_init();
net_ns_init();
vfs_caches_init();/* 虚拟文件系统缓存初始化 */
pagecache_init();
signals_init();/* 初始化信号 */
seq_file_init();
proc_root_init();/* 注册并挂载 proc 文件系统 */
nsfs_init();
/* 初始化 cpuset,cpuset 是将 CPU 和内存资源以逻辑性
* 和层次性集成的一种机制,是 cgroup 使用的子系统之一
*/
cpuset_init();
cgroup_init();/* 初始化 cgroup */
taskstats_init_early();/* 进程状态初始化 */
delayacct_init();
poking_init();
check_bugs();/* 检查写缓冲一致性 */
acpi_subsystem_init();
arch_post_acpi_subsys_init();
kcsan_init();
/* 调用 rest_init 函数 */
/* 创建 init、kthread、idle 线程 */
arch_call_rest_init();
prevent_tail_call_optimization();
}
其中有七个函数较为重要,分别为:
setup_arch(&command_line);
mm_init();
sched_init();
init_IRQ();
console_init();
vfs_caches_init();
rest_init();
1、setup_arch(&command_line)
此函数是系统架构初始化函数,处理 uboot 传递进来的参数,不同的架构进行不同的初始化,也就是说每个架构都会有一个 setup_arch 函数。
linux4.14/arch/arm/kernel/setup.c
void __init setup_arch(char **cmdline_p)
{
const struct machine_desc *mdesc;
setup_processor();
mdesc = setup_machine_fdt(__atags_pointer);
if (!mdesc)
mdesc = setup_machine_tags(__atags_pointer, __machine_arch_type);
machine_desc = mdesc;
machine_name = mdesc->name;
dump_stack_set_arch_desc("%s", mdesc->name);
if (mdesc->reboot_mode != REBOOT_HARD)
reboot_mode = mdesc->reboot_mode;
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;
/* populate cmd_line too for later use, preserving boot_command_line */
strlcpy(cmd_line, boot_command_line, COMMAND_LINE_SIZE);
*cmdline_p = cmd_line;
early_fixmap_init();
early_ioremap_init();
parse_early_param();
#ifdef CONFIG_MMU