内核的Makefile与链接脚本
1、kernel的Makefile写法和规则等和uboot的Makefile很类似;
2、有几点可能涉及到更改:一、Makefile中刚开始定义了kernel的内核版本号;二、Makefile中定义了2个变量,ARCH(ARCH = arm的时候,将来在源码目录下去操作的arch/arm目录)、CROSS_COMPILE(两者都可以在make时通过命令行传参,且优先级更高);
3、链接脚本(arch/arm/kernel/目录下)中确定整个程序的entry为ENTRY(stext);
4、链接脚本并不是直接提供的,而是提供了一个汇编文件vmlinux.lds.S,然后在编译的时候再去编译这个文件得到真正的链接脚本vmlinux.lds(猜测:.lds文件中只能写死,不能用条件编译,但kernel中链接脚本确实有条件编译的需求);
内核启动的汇编阶段
head.S中开头定义了2个宏:KERNEL_RAM_VADDR、KERNEL_RAM_PADDR,分别表示内核运行时所在的虚拟地址、内核运行时所在的物理地址。
在分析程序入口ENTRY(stext)之前,有一段注释,解读一下。
这里通常是被解压代码所调用,被调用的要求如下:MMU关闭,D-cache关闭,I-cache不关心,r0等于0,r1为机器码,r2为传参atags的地址。
这段代码必须是位置无关码,所以如果你把内核链接到了0xc0008000,你可以调用__pa(0xc0008000)将虚拟地址转成物理地址(kernel启动时MMU是关闭的,因此涉及到操作硬件寄存器等时必须使用物理地址)。
关于r1的机器码可以查看linux/arch/arm/tools/mach-types这个文件。
尽量保证这段代码的简洁,不要添加任何与硬件操作相关的代码(这些应该是boot loader做的)。
stext这段代码:
1、关中断、设置svc模式
2、校验传参
__lookup_processor_type、__lookup_machine_type分别是校验cpuid和machid的合法性(是否在内核维护的cpuid/machid列表中能找到),不合法则错误返回;__vet_atags校验atag传参首位是否为ATAG_CORE;
3、建立段式页表
__create_page_tables,linux内核本身被链接在虚拟地址处,希望尽快建立页表并且启动MMU进入虚拟地址工作状态。
kernel建立页表分为2步:第一步,kernel先建立了一个段式页表(和uboot中建立的页表一样,段式页表1MB一个映射,4GB空间需要4096个页表项,每个页表项4字节,因此一共需要16KB内存来做页表);第二步,再去建立一个细页表(4kb为单位的细页表),然后启用新的细页表废除第一步建立的段式映射页表。
4、接下来的对r13(sp)赋值一个函数指针数组__switch_data;
5、开启MMU
6、跳转__mmap_switched,复制数据段、清除bss段(目的是构建C语言运行环境)、保存cpuid、机器码、tag传参的首地址,start_kernel跳转到C语言运行阶段;
参考:从stext到start_kernel
内核启动的C语言阶段
start_kernel函数在init/main.c中。C语言阶段的函数中,我们只分析重要的、跟启动流程相关的一些内容:
- 分析uboot给kernel传参的影响和实现(传参搞定了,启动时的各种初始化自动就正确了);
- 硬件初始化与驱动加载;
- 内核启动后的结局与归宿;
编译信息打印
printk(KERN_NOTICE "%s", linux_banner);
printk打印Linux版本、编译用户名、编译主机名、编译器版本、编译时间等。
setup_arch函数
这个函数主要做了2件事,通过machid找到一个设备描述实体mdesc(类型为machine_desc),以及comline的确认。
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);
printk("@@@@@@@ atags_pointer not null\n");
}
else if (mdesc->boot_params)
{
tags = phys_to_virt(mdesc->boot_params);
printk("@@@@@@@ boot params not null\n");
}
printk("@@@@@@@linter#####boot_params:%p,mdesc->boot_params:%p\n",tags);
/*
* 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;
printk("$$$$$$$$$cmdline:%s\n",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();
}
首先通过mdesc = setup_machine(machine_arch_type);
获取mdesc(类型为struct machine_desc的变量,用于描述一个machine),传入的machine_arch_type其实就是机器码(见include/generated/mach-types.h)。
获取mdesc的流程如下:
setup_machine =》 lookup_machine_type(汇编) =》 __lookup_machine_type(汇编)
这个查找的地址是__arch_info_begin到__arch_info_end,查看链接脚本可知该地址属于 .arch.info.init 段。
这里是取用的地方,放的地方在mach-xxx.c中的MACHINE_START宏定义了一个具体machine,并内核代码段.arch.info.init中。例如:
#ifdef CONFIG_MACH_SMDKC110
MACHINE_START(SMDKC110, "SMDKC110")
#elif CONFIG_MACH_SMDKV210
MACHINE_START(SMDKV210, "SMDKV210")
#endif
/* Maintainer: Kukjin Kim <kgene.kim@samsung.com> */
.phys_io = S3C_PA_UART & 0xfff00000,
.io_pg_offst = (((u32)S3C_VA_UART) >> 18) & 0xfffc,
.boot_params = S5P_PA_SDRAM + 0x100,
//.fixup = smdkv210_fixup,
.init_irq = s5pv210_init_irq,
.map_io = smdkc110_map_io,
.init_machine = smdkc110_machine_init,
.timer = &s5p_systimer,
MACHINE_END
comline的确认:
char *from = default_command_line;
局部变量from被赋值default_command_line,default_command_line是一个全局变量数组,被赋初值为CONFIG_CMDLINE,而CONFIG_CMDLINE是.config中的一个配置项(意味着menuconfig的时候是可以进行配置的,表示内核的一个默认的命令行参数)。在下面的会被重新赋值为atag传参的那个(也就是uboot中的bootargs),然后被拷贝到各个变量中去。
然后setup_arch函数就没有什么重要的了。返回出来接着看。
解析cmdline传参:parse_early_param和parse_args这两个函数是在解析cmdline传参,解析成字符串数组。
譬如cmdline:console=ttySAC2,115200 root=/dev/mmcblk0p2 rw init=/linuxrc rootfstype=ext3
解析后:
"console=ttySAC2,115200"
"root=/dev/mmcblk0p2 rw"
"init=/linuxrc"
"rootfstype=ext3"
后续执行的各自模块初始化代码,依赖于这些参数。
接下来是一系列模块的初始化:
(1)trap_init 设置异常向量表
(2)mm_init 内存管理模块初始化
(3)sched_init 内核调度系统初始化
(4)early_irq_init&init_IRQ 中断初始化
(5)console_init 控制台初始化
最后调用rest_init();
,然后start_kernel函数就结束了。
总结:start_kernel函数做的主要工作:打印了一些信息、内核工作需要的模块的初始化被依次调用(譬如内存管理、调度系统、异常处理以及驱动加载···)、我们需要重点了解的就是setup_arch中做的2件事情:机器码架构的查找并且执行架构相关的硬件的初始化、uboot给内核的传参cmdline。
rest_init函数
这个函数之前内核的基本组装已经完成。剩下的一些工作就比较重要了,放在了一个单独的函数rest_init中。
static noinline void __init_refok rest_init(void)
__releases(kernel_lock)
{
int pid;
rcu_scheduler_starting();
/*
* We need to spawn init first so that it obtains pid 1, however
* the init task will end up wanting to create kthreads, which, if
* we schedule it before we create kthreadd, will OOPS.
*/
kernel_thread(kernel_init, NULL, CLONE_FS | CLONE_SIGHAND);
numa_default_policy();
pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);
rcu_read_lock();
kthreadd_task = find_task_by_pid_ns(pid, &init_pid_ns);
rcu_read_unlock();
complete(&kthreadd_done);
unlock_kernel();
/*
* The boot idle thread must execute schedule()
* at least once to get things moving:
*/
init_idle_bootup_task(current);
preempt_enable_no_resched();
schedule();
preempt_disable();
/* Call into cpu_idle with preempt disabled */
cpu_idle();
}
rest_init中调用kernel_thread函数启动了2个内核线程,分别是:kernel_init(进程1,init进程)和kthreadd(进程2,内核守护进程),调用schedule();
将CPU调度出去,从此linux系统开始转起来了。
最后,当CPU再次调度回来的时候,调用cpu_idle();
进入idle进程,在其中while(1)死循环(内核进程0),一般会做这几件事:
- 统计idle空闲时间;
- 调用
pm_idle();
进入省电模式;可参考Linux : CPU Idle - 调用
schedule();
让CPU去调度其他进程。
/*
* The idle thread, has rather strange semantics for calling pm_idle,
* but this is what x86 does and we need to do the same, so that
* things like cpuidle get called in the same way. The only difference
* is that we always respect 'hlt_counter' to prevent low power idle.
*/
void cpu_idle(void)
{
local_fiq_enable();
/* endless idle loop with no priority at all */
while (1) {
tick_nohz_stop_sched_tick(1);
leds_event(led_idle_start);
while (!need_resched()) {
#ifdef CONFIG_HOTPLUG_CPU
if (cpu_is_offline(smp_processor_id()))
cpu_die();
#endif
local_irq_disable();
if (hlt_counter) {
local_irq_enable();
cpu_relax();
} else {
stop_critical_timings();
pm_idle();
start_critical_timings();
/*
* This will eventually be removed - pm_idle
* functions should always return with IRQs
* enabled.
*/
WARN_ON(irqs_disabled());
local_irq_enable();
}
}
leds_event(led_idle_end);
tick_nohz_restart_sched_tick();
preempt_enable_no_resched();
schedule();
preempt_disable();
}
}
init进程解析
init进程完成了从内核态向用户态的转变(一个进程2种状态):init进程刚开始运行的时候是内核态,然后他自己运行了一个用户态下面的程序后把自己强行转成了用户态。
内核态下做了什么?重点做了一件事情,就是挂载根文件系统并试图找到用户态下的那个init程序,并执行。
用户态下做了什么?init进程大部分有意义的工作都是在用户态下进行的。用户态下的其他进程都是由init进程直接或间接创建的。特别是:(1)init启动了login进程、命令行进程、shell进程(命令行负责输入与显示,shell负责解析);(2)shell进程启动了其他用户进程。命令行和shell一旦工作了,用户就可以在命令行下通过./xx的方式来执行其他应用程序(进程)。
代码流程分析
static int __init kernel_init(void * unused)
{
/*
* Wait until kthreadd is all set-up.
*/
wait_for_completion(&kthreadd_done);
lock_kernel();
/*
* init can allocate pages on any node
*/
set_mems_allowed(node_states[N_HIGH_MEMORY]);
/*
* init can run on any cpu.
*/
set_cpus_allowed_ptr(current, cpu_all_mask);
/*
* Tell the world that we're going to be the grim
* reaper of innocent orphaned children.
*
* We don't want people to have to make incorrect
* assumptions about where in the task array this
* can be found.
*/
init_pid_ns.child_reaper = current;
cad_pid = task_pid(current);
smp_prepare_cpus(setup_max_cpus);
do_pre_smp_initcalls();
start_boot_trace();
smp_init();
sched_init_smp();
do_basic_setup();
/* Open the /dev/console on the rootfs, this should never fail */
if (sys_open((const char __user *) "/dev/console", O_RDWR, 0) < 0)
printk(KERN_WARNING "Warning: unable to open an initial console.\n");
(void) sys_dup(0);
(void) sys_dup(0);
/*
* check if there is an early userspace init. If yes, let it do all
* the work
*/
if (!ramdisk_execute_command)
ramdisk_execute_command = "/init";
if (sys_access((const char __user *) ramdisk_execute_command, 0) != 0) {
ramdisk_execute_command = NULL;
prepare_namespace();
}
/*
* Ok, we have completed the initial bootup, and
* we're essentially up and running. Get rid of the
* initmem segments and start the user-mode stuff..
*/
init_post();
return 0;
}
打开控制台:
这里打开了/dev/console 文件,并且复制了2次文件描述符,一共得到了3个文件描述符。
这三个文件描述符分别是0、1、2.这三个文件描述符就是所谓的:标准输入、标准输出、标准错误。因此后续的进程1衍生出来的所有的进程默认都具有这3个三件描述符。
挂载根文件系统:
prepare_namespace函数中挂载根文件系统。uboot传参中的root=/dev/mmcblk0p2 rw 告诉内核根文件系统在哪里,rootfstype=ext3 告诉内核rootfs的类型。
如果内核挂载根文件系统成功,则会打印出(xxx表示文件系统类型):VFS: Mounted root (xxx filesystem) on device
执行用户态下的进程1程序:
/* This is a non __init function. Force it to be noinline otherwise gcc
* makes it inline to init() and it becomes part of init.text section
*/
static noinline int init_post(void)
__releases(kernel_lock)
{
/* need to finish all async __init code before freeing the memory */
async_synchronize_full();
free_initmem();
unlock_kernel();
mark_rodata_ro();
system_state = SYSTEM_RUNNING;
numa_default_policy();
current->signal->flags |= SIGNAL_UNKILLABLE;
if (ramdisk_execute_command) {
run_init_process(ramdisk_execute_command);
printk(KERN_WARNING "Failed to execute %s\n",
ramdisk_execute_command);
}
/*
* We try each of these until one succeeds.
*
* The Bourne shell can be used instead of init if we are
* trying to recover a really broken machine.
*/
if (execute_command) {
run_init_process(execute_command);
printk(KERN_WARNING "Failed to execute %s. Attempting "
"defaults...\n", execute_command);
}
run_init_process("/sbin/init");
run_init_process("/etc/init");
run_init_process("/bin/init");
run_init_process("/bin/sh");
panic("No init found. Try passing init= option to kernel. "
"See Linux Documentation/init.txt for guidance.");
}
上面一旦挂载rootfs成功,则进入rootfs中寻找应用程序的init程序,找到后用run_init_process(内部调用了kernel_execve)去执行它。
确定init程序的顺位:uboot传参cmdline中指定的程序,如init=/linuxrc;然后就是默认的四个默认路径,依次为:/sbin/init、/etc/init、/bin/init、/bin/sh;如果都没找到,那么内核就panic了。