linux kernel v6.6.31(LTS)
start_kernel()的实现在 /init/main.c
asmlinkage __visible __init __no_sanitize_address __noreturn __no_stack_protector
void start_kernel(void)
先解释一手上面一大串宏的作用:
asmlinkage
: 这是一个汇编语言链接约定,用于告诉编译器这个函数的参数是从栈上获取的,而不是通过寄存器。在x86架构中,所有内核的入口点(包括中断和系统调用处理程序)都使用asmlinkage,因为当这些函数被调用时,寄存器可能正在被其他用途。__visible
: 这个宏确保函数对于链接器是可见的,这样它就可以被正确地放置在内核的二进制映像中,并可以被其他模块引用。__init
: 这个标记告诉编译器这个函数只在初始化阶段需要,一旦初始化完成,这个函数就不再需要了。编译器会因此将这个函数的代码放置在内核映像的一个特殊段中,在初始化之后这个段可以被释放,从而节省内存资源。__no_sanitize_address
: 这个修饰符用于告诉AddressSanitizer(一种内存错误检测工具)不要对函数进行检测。这通常用于那些不遵循普通内存访问规则的底层代码。__noreturn
: 这个修饰符表明函数不会返回给调用者。对于start_kernel来说,这个函数是内核执行的起点,它不会返回到之前的执行环境。__no_stack_protector
: 这个修饰符用于关闭堆栈保护,这通常用于那些对性能要求极高或者在特定情况下堆栈保护会导致问题的代码。
由于start_kernel()实在是太长,下面分段解释各个变量和函数的作用,必要时会给出子一级函数的定义和实现。下面看以下几个变量和初始化函数或宏:
char* command_line;
char* after_dashes;
以上两个变量存储引导过程中传递给内核的参数,可以用来配置内核相关功能
set_task_stack_end_magic(&init_task);
smp_setup_processor_id();
debug_objects_early_init();
init_vmlinux_build_id();
cgroup_init_early();
local_irq_disable();
early_boot_irqs_disabled = true;
set_task_stack_end_magic(&init_task)
impl in /kernel/fork.c
void set_task_stack_end_magic(struct task_struct *tsk)
{
unsigned long *stackend;
stackend = end_of_stack(tsk);
*stackend = STACK_END_MAGIC; /* for overflow detection */
}
这个函数用于设置init进程(PID 0)的栈结束魔数(magic number)。这个魔数用于检测栈溢出。init_task是内核中的第一个任务,是所有其他任务的祖先。这个函数确保init_task的栈有一个已知的结束标记,以便在栈溢出时可以检测到。
魔数(magic number):特别的没有什么意义数值,此处的魔数是定义在`/include/uapi/linux/magic.h`中的`STACK_END_MAGIC`(0x57AC6E9D)
smp_setup_processor_id()
这个函数用于在多处理器(SMP)系统中设置当前处理器的ID。在SMP系统中,每个CPU都需要知道自己的身份,以便内核可以正确地管理并发执行和处理器间的通信。debug_objects_early_init()
impl in /lib/debugobjects.c
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);
}
这个函数用于初始化内核的调试对象子系统。调试对象是一种内核基础设施,用于跟踪内核中的对象(如锁、引用计数等)的生命周期,以帮助开发者发现和修复内存泄漏和死锁等问题。
init_vmlinux_build_id()
impl in /lib/buildid.c
void __init init_vmlinux_build_id(void)
{
extern const void __start_notes __weak;
extern const void __stop_notes __weak;
unsigned int size = &__stop_notes - &__start_notes;
build_id_parse_buf(&__start_notes, vmlinux_build_id, size);
}
这个函数用于初始化内核映像的构建ID。构建ID是一个唯一标识内核构建的字符串,通常包含构建的时间戳和Git哈希值。这个ID可以帮助开发者追踪内核的版本和构建信息。
cgroup_init_early()
impl in /kernel/cgroup/cgroup.c
int __init cgroup_init_early(void)
{
static struct cgroup_fs_context __initdata ctx;
struct cgroup_subsys *ss;
int i;
ctx.root = &cgrp_dfl_root;
init_cgroup_root(&ctx);
cgrp_dfl_root.cgrp.self.flags |= CSS_NO_REF;
RCU_INIT_POINTER(init_task.cgroups, &init_css_set);
for_each_subsys(ss, i) {
WARN(!ss->css_alloc || !ss->css_free || ss->name || ss->id,
"invalid cgroup_subsys %d:%s css_alloc=%p css_free=%p id:name=%d:%s\n",
i, cgroup_subsys_name[i], ss->css_alloc, ss->css_free,
ss->id, ss->name);
WARN(strlen(cgroup_subsys_name[i]) > MAX_CGROUP_TYPE_NAMELEN,
"cgroup_subsys_name %s too long\n", cgroup_subsys_name[i]);
ss->id = i;
ss->name = cgroup_subsys_name[i];
if (!ss->legacy_name)
ss->legacy_name = cgroup_subsys_name[i];
if (ss->early_init)
cgroup_init_subsys(ss, true);
}
return 0;
}
这个函数用于初始化控制组(cgroups)的早期阶段。cgroups是Linux内核的一个功能,用于限制、计算和隔离进程组使用的资源(如CPU、内存、网络等)。
local_irq_disable()
impl in /include/linux/irqflags.h
定义了CONFIG_TRACE_IRQFLAGS
的话local_irq_disable()
的实现是
#define local_irq_disable() \
do { \
bool was_disabled = raw_irqs_disabled();\
raw_local_irq_disable(); \
if (!was_disabled) \
trace_hardirqs_off(); \
} while (0)
否则为
#define local_irq_disable() do { raw_local_irq_disable(); } while (0)
这个宏用于在当前处理器上禁用本地中断。在内核初始化的早期阶段,中断可能会干扰内核的初始化过程,因此有时需要禁用它们。
early_boot_irqs_disabled = true
这行代码将early_boot_irqs_disabled
变量设置为true
,表示在内核初始化的早期阶段,中断被禁用了。这个变量用于跟踪中断的状态,以确保在适当的时候重新启用中断。
boot_cpu_init();
page_address_init();
pr_notice("%s", linux_banner);
early_security_init();
setup_arch(&command_line);
setup_boot_config();
setup_command_line(command_line);
setup_nr_cpu_ids();
setup_per_cpu_areas();
smp_prepare_boot_cpu(); /* arch-specific boot-cpu hooks */
boot_cpu_hotplug_init();
boot_cpu_init()
impl in /kernel/cpu.c
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);
#ifdef CONFIG_SMP
__boot_cpu_id = cpu;
#endif
}
初始化引导CPU(bootstrapping CPU)的数据结构。这通常包括设置CPU的标识信息,如CPU ID,以及可能的特定于CPU的初始化操作。
page_address_init()
初始化页到物理地址的映射。这允许内核通过页结构访问实际的物理内存地址。pr_notice("%s", linux_banner)
打印Linux内核的版本信息,这通常是在内核启动日志中看到的第一个信息,用于显示内核版本、编译器版本等信息。early_security_init()
impl in /security/security.c
int __init early_security_init(void)
{
struct lsm_info *lsm;
#define LSM_HOOK(RET, DEFAULT, NAME, ...) \
INIT_HLIST_HEAD(&security_hook_heads.NAME);
#include "linux/lsm_hook_defs.h"
#undef LSM_HOOK
for (lsm = __start_early_lsm_info; lsm < __end_early_lsm_info; lsm++) {
if (!lsm->enabled)
lsm->enabled = &lsm_enabled_true;
prepare_lsm(lsm);
initialize_lsm(lsm);
}
return 0;
}
初始化内核的早期安全特性,如安全增强Linux(SELinux)或其他安全模块的早期设置。
setup_arch(&command_line)
impl in/arch/<arch>/kernel/setup.c
执行与体系结构相关的初始化。这个函数非常依赖于具体的硬件架构,它负责设置内核的中断处理、内存管理、CPU调度等。setup_boot_config()
impl in /init/main.c
static void __init setup_boot_config(void)
{
static char tmp_cmdline[COMMAND_LINE_SIZE] __initdata;
const char *msg, *data;
int pos, ret;
size_t size;
char *err;
/* Cut out the bootconfig data even if we have no bootconfig option */
data = get_boot_config_from_initrd(&size);
/* If there is no bootconfig in initrd, try embedded one. */
if (!data)
data = xbc_get_embedded_bootconfig(&size);
strscpy(tmp_cmdline, boot_command_line, COMMAND_LINE_SIZE);
err = parse_args("bootconfig", tmp_cmdline, NULL, 0, 0, 0, NULL,
bootconfig_params);
if (IS_ERR(err) || !(bootconfig_found || IS_ENABLED(CONFIG_BOOT_CONFIG_FORCE)))
return;
/* parse_args() stops at the next param of '--' and returns an address */
if (err)
initargs_offs = err - tmp_cmdline;
if (!data) {
/* If user intended to use bootconfig, show an error level message */
if (bootconfig_found)
pr_err("'bootconfig' found on command line, but no bootconfig found\n");
else
pr_info("No bootconfig data provided, so skipping bootconfig");
return;
}
if (size >= XBC_DATA_MAX) {
pr_err("bootconfig size %ld greater than max size %d\n",
(long)size, XBC_DATA_MAX);
return;
}
ret = xbc_init(data, size, &msg, &pos);
if (ret < 0) {
if (pos < 0)
pr_err("Failed to init bootconfig: %s.\n", msg);
else
pr_err("Failed to parse bootconfig: %s at %d.\n",
msg, pos);
} else {
xbc_get_info(&ret, NULL);
pr_info("Load bootconfig: %ld bytes %d nodes\n", (long)size, ret);
/* keys starting with "kernel." are passed via cmdline */
extra_command_line = xbc_make_cmdline("kernel");
/* Also, "init." keys are init arguments */
extra_init_args = xbc_make_cmdline("init");
}
return;
}
static void __init setup_boot_config(void)
{
/* Remove bootconfig data from initrd */
get_boot_config_from_initrd(NULL);
}
设置引导配置, __init标识下说明setup_boot_config()函数是内核函数,链接器有特殊的处理保证不会出现链接错误(具体是什么机制,我也没有查到准确说法)
补充
在Linux操作系统中,initrd(Initial RAM Disk)是一个临时的根文件系统,它在内核启动的早期阶段被加载到内存中,用于提供必要的文件和驱动程序,以便内核能够访问真正的根文件系统。initrd通常被用于当内核还没有加载所有必要的驱动程序来访问硬盘上的文件系统时,或者当根文件系统需要通过网络启动时。
initrd的主要作用如下:
提供驱动
: initrd可以包含必要的驱动程序,特别是那些用于磁盘和文件系统的驱动程序,这些驱动程序可能不在内核映像中。这样,即使内核本身不支持某种存储设备,也可以通过initrd中的驱动程序来访问。准备根文件系统
: initrd可以包含工具和脚本,用于挂载真正的根文件系统。例如,它可以包含逻辑卷管理(LVM)或软件RAID的工具,这些工具可以帮助挂载复杂的文件系统配置。
转换根文件系统: 在某些情况下,initrd可以用于转换根文件系统的状态,例如,从只读状态切换到读写状态,或者从网络文件系统切换到本地文件系统。引导加载
: initrd可以用于网络引导(PXE)的情况,其中整个操作系统是通过网络下载的,包括内核和根文件系统。
initrd通常是一个压缩的文件系统映像,它在引导过程中由引导加载程序(如GRUB uBoot LILO)加载到内存中。一旦内核启动并运行,它会解压initrd并将其作为临时根文件系统使用。然后,内核会执行initrd中的/init脚本,该脚本负责初始化硬件、加载必要的驱动程序,并最终挂载真正的根文件系统。
在现代Linux系统中,initrd已经被initramfs(Initial RAM File System)所取代。initramfs是一个基于cpio的文件系统映像,它可以在引导时动态地解压到根文件系统中。与initrd相比,initramfs更加灵活,因为它可以动态地调整大小以适应不同的引导情况。
setup_command_line(command_line)
impl in /init/main.c
/*
* 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)
{
size_t len, xlen = 0, ilen = 0;
if (extra_command_line)
xlen = strlen(extra_command_line);
if (extra_init_args)
ilen = strlen(extra_init_args) + 4; /* for " -- " */
len = xlen + strlen(boot_command_line) + 1;
saved_command_line = memblock_alloc(len + ilen, SMP_CACHE_BYTES);
if (!saved_command_line)
panic("%s: Failed to allocate %zu bytes\n", __func__, len + ilen);
len = xlen + strlen(command_line) + 1;
static_command_line = memblock_alloc(len, SMP_CACHE_BYTES);
if (!static_command_line)
panic("%s: Failed to allocate %zu bytes\n", __func__, len);
if (xlen) {
/*
* We have to put extra_command_line before boot command
* lines because there could be dashes (separator of init
* command line) in the command lines.
*/
strcpy(saved_command_line, extra_command_line);
strcpy(static_command_line, extra_command_line);
}
strcpy(saved_command_line + xlen, boot_command_line);
strcpy(static_command_line + xlen, command_line);
if (ilen) {
/*
* Append supplemental init boot args to saved_command_line
* so that user can check what command line options passed
* to init.
* The order should always be
* " -- "[bootconfig init-param][cmdline init-param]
*/
if (initargs_offs) {
len = xlen + initargs_offs;
strcpy(saved_command_line + len, extra_init_args);
len += ilen - 4; /* strlen(extra_init_args) */
strcpy(saved_command_line + len,
boot_command_line + initargs_offs - 1);
} else {
len = strlen(saved_command_line);
strcpy(saved_command_line + len, " -- ");
len += 4;
strcpy(saved_command_line + len, extra_init_args);
}
}
saved_command_line_len = strlen(saved_command_line);
}
处理内核命令行参数,这些参数通常由引导加载程序(如GRUB, LILO)传递给内核。
setup_nr_cpu_ids()
设置系统中CPU的总数。这告诉内核有多少个CPU需要初始化和调度。setup_per_cpu_areas()
为每个CPU分配和初始化 per-cpu 数据区域,用于存储每个CPU的私有数据,如寄存器状态等。smp_prepare_boot_cpu()
为引导CPU准备 SMP(对称多处理)环境设置CPU间通信的必要数据结构和同步机制。boot_cpu_hotplug_init()
初始化CPU热插拔支持。允许在系统运行时动态地添加或移除CPU。