在上一篇中,Linux内核已经在arch/x86/kernel/head64.c中调用start_kernel,已经进入内核入口点。在start_kernel函数是与体系架构无关的通用处理入口函数,尽管我们在此初始化过程中需要无数次返回arch文件夹。我们接下来分析其处理过程。
start_kernel函数在init/main.c中定义。如下:
asmlinkage __visible void __init start_kernel(void)
{
char *command_line;
char *after_dashes;
...
}可以看到start_kernel函数使用了__visible和__init特性。__visible特性告诉编译器其他函数在使用该函数或变量,为了防止标记这个函数或变量是unusable。在内核初始化阶段所有的函数都需要使用__init特性,其定义如下:
#define __init __section(.init.text) __cold __latent_entropy __noinitretpoline在初始化完成后,内核通过调用free_initmem来释放这些段(section)。可以看到__init通过其他几个属性定义的,__cold属性用来标记该函数很少使用,编译器必须优化此函数的大小。
这是进入start_kernel后第一个调用的函数。在kernel/fork.c中实现。该函数获取init_task的栈尾并将其设置为STACK_END_MAGIC(0x57AC6E9D)。如下:
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_task表示初始化进程(或任务)的数据结构,在init/init_task.c中定义。如下:
struct task_struct init_task
#ifdef CONFIG_ARCH_TASK_STRUCT_ON_STACK
__init_task_data
#endif
= {
#ifdef CONFIG_THREAD_INFO_IN_TASK
.thread_info = INIT_THREAD_INFO(init_task),
.stack_refcount = REFCOUNT_INIT(1),
#endif
.state = 0,
.stack = init_stack,
.usage = REFCOUNT_INIT(2),
.flags = PF_KTHREAD,
...
#ifdef CONFIG_SECURITY
.security = NULL,
#endif
};
EXPORT_SYMBOL(init_task);init_task是一个struct task_struct结构体,它存储了有关进程的所有信息,在include/linux/sched.h中定义。struct task_struct的结构体比较庞大,包含了100多个字段,我们会经常使用到它,它是Linux内核中进程(Process)的基本结构。init_task设置和初始化了第一个进程的值,设置如下:
- 初始化
state为0(或者,runnable),即:一个等待CPU运行的进程; - 初始化
flags为PF_KTHREAD,即:内核线程; - 初始化栈信息
stack为init_stack; - 可运行的任务列表
tasks; - 内存地址空间
active_mm; - 初始化
thread_info; - ...
init_stack在include/asm-generic/vmlinux.lds.h定义,如下:
#define INIT_TASK_DATA(align) \
. = ALIGN(align); \
__start_init_task = .; \
init_thread_union = .; \
init_stack = .; \
KEEP(*(.data..init_task)) \
KEEP(*(.data..init_thread_info)) \
. = __start_init_task + THREAD_SIZE; \
__end_init_task = .;thread_info结构体在arch/x86/include/asm/thread_info.h中定义,只有flags和status两个字段,如下:
struct thread_info {
unsigned long flags; /* low level flags */
u32 status; /* thread synchronous flags */
};- SMP设置处理器ID
smp_setup_processor_id函数在x86_64平台下是个空函数,该函数在一部分平台(如:arm64等)下实现。
- 调试信息早期初始化
debug_objects_early_init函数根据CONFIG_DEBUG_OBJECTS内核配置选项实现不同。在开启的情况下,在lib/debugobjects.c中实现,填充obj_hash和obj_static_pool调试信息。
- cgroup早期初始化
cgroup_init_early函数在kernel/cgroup/cgroup.c中实现,进行cgroup相关初始化。
接下来,我们需要禁用本地IRQ。通过调用include/linux/irqflags.h中的local_irq_disable函数来实现。local_irq_disable最终会调用arch_local_irq_disable。
arch_local_irq_disable根据平台的不同实现不同。在x86_64下,调用native_irq_disable,最终调用cli指令。
boot_cpu_init在kernel/cpu.c中实现。
- 获取当前处理器ID
首先,我们需要获取当前CPU的ID,通过smp_processor_id获取。smp_processor_id在include/linux/smp.h展开为__smp_processor_id。目前是0,在CONFIG_SMP的情况下,在arch/x86/include/asm/smp.h展开为this_cpu_read(cpu_number)。
this_cpu_read同其他函数一样(如:this_cpu_write, this_cpu_and, this_cpu_or等)定义在include/linux/percpu-defs.h中,提供对percpu变量的访问。以this_cpu_read为例,
#define this_cpu_read(pcp) __pcpu_size_call_return(this_cpu_read_, pcp)
#define __pcpu_size_call_return(stem, variable) \
({ \
typeof(variable) pscr_ret__; \
__verify_pcpu_ptr(&(variable)); \
switch(sizeof(variable)) { \
case 1: pscr_ret__ = stem##1(variable); break; \
case 2: pscr_ret__ = stem##2(variable); break; \
case 4: pscr_ret__ = stem##4(variable); break; \
case 8: pscr_ret__ = stem##8(variable); break; \
default: \
__bad_size_call_parameter(); break; \
} \
pscr_ret__; \
})__pcpu_size_call_return的实现很简单,但是比较奇怪。pscr_ret__变量定义为int类型,是因为variable是cpu_number,它的定义如下:
DECLARE_PER_CPU_READ_MOSTLY(int, cpu_number);- 验证CPU指针变量
接下来,调用__verify_pcpu_ptr来验证cpu_number的地址是否一个有效的precpu变量指针。然后,根据variable的占用的类型大小来获取pscr_ret__。我们的cpu_number变量是int类型(即4个字节),因此,我们执行pscr_ret__ = this_cpu_read_4(cpu_number)。
this_cpu_read_4是个宏定义,最终调用汇编语句,展开如下:
#define this_cpu_read_4(pcp) percpu_from_op(volatile, "mov", pcp)
#define percpu_from_op(qual, op, var) \
({ \
typeof(var) pfo_ret__; \
switch (sizeof(var)) { \
case 1: \
...
break; \
case 2: \
...
break; \
case 4: \
asm qual (op "l "__percpu_arg(1)",%0" \
: "=r" (pfo_ret__) \
: "m" (var)); \
break; \
case 8: \
...
break; \
default: __bad_percpu_size(); \
} \
pfo_ret__; \
})由于,我们没有设置percpu区域,目前只有一个CPU,smp_processor_id的返回结果为0。
- 设置CPU状态
在得到当前处理器id后,boot_cpu_init设置CPU的在线、激活状态,包括:online,active,present,possible,如下:
set_cpu_online(cpu, true);
set_cpu_active(cpu, true);
set_cpu_present(cpu, true);
set_cpu_possible(cpu, true);上述我们使用的这些CPU配置称为CPU掩码(cpumask)。cpu_possible_mask表示可填充的CPU;cpu_present_mask表示已填充的CPU;cpu_online_mask表示可用于调度程序的CPU;cpu_active_mask表示可用于迁移的CPU。这些设置功能相似,通过第二个参数来调用cpumask_set_cpu或cpumask_clear_cpu来改变对应cpumask的状态。cpumask的定义如下:
typedef struct cpumask { DECLARE_BITMAP(bits, NR_CPUS); } cpumask_t;
#define DECLARE_BITMAP(name,bits) \
unsigned long name[BITS_TO_LONGS(bits)]可以看到cpumask_t是一个unsigned long类型的数组,使用bitmap来表示当前系统中CPU,每个CPU使用1bit。
cpumask_set_cpu或cpumask_clear_cpu最终通过set_bit或clear_bit来改变对应的状态。如下:
static inline void
set_cpu_active(unsigned int cpu, bool active)
{
if (active)
cpumask_set_cpu(cpu, &__cpu_active_mask);
else
cpumask_clear_cpu(cpu, &__cpu_active_mask);
}
static inline void cpumask_set_cpu(unsigned int cpu, struct cpumask *dstp)
{
set_bit(cpumask_check(cpu), cpumask_bits(dstp));
}
static inline void cpumask_clear_cpu(int cpu, struct cpumask *dstp)
{
clear_bit(cpumask_check(cpu), cpumask_bits(dstp));
}调用pr_notice函数(printk的扩展),打印Linux的banner,包括内核版本以及编译环境信息。
#define pr_notice(fmt, ...) \
pr_printk_hash(KERN_NOTICE, fmt, ##__VA_ARGS__)
#define pr_printk_hash(level, format, ...) \
printk(level pr_fmt(format), ##__VA_ARGS__)linux_banner在init/version.c定义,如下:
const char linux_banner[] =
"Linux version " UTS_RELEASE " (" LINUX_COMPILE_BY "@"
LINUX_COMPILE_HOST ") (" LINUX_COMPILER ") " UTS_VERSION "\n";- 页地址初始化
page_address_init函数在内存不能直接映射时(如:highmem)执行,在当前情况下为空函数。
- 早期安全初始化
early_security_init在security/security.c实现,进行安全早期初始化。
接下来,调用setup_arch函数进行平台相关设置。
本文描述了Linux内核平台入口函数,主要进行init进程的设置和CPU的设置。
本系列文章翻译自linux-insides,如果你有任何问题或者建议,请联系0xAX或者创建 issue。