简介
参考
下面我们来分析典型得Linux启动过程。
首先。 我们需要直到三个重要得组件,
- bootloader
- kernel
- rootfs.
首先bootloader引导这一块就无需讨论。直接分析kernel得操作。
下面是大致得启动流程。
0号进程
这个进程又可以叫做Idle进程。是唯一一个没有通过fork或者kernel_thread产生的进程。完成系统加载后,演变成为进程调度。
idle创建
init_task是内核中所有进程、线程的task_struct雏形,在内核初始化过程中,通过静态定义构造出了一个task_struct接口,取名为init_task,然后在内核初始化的后期,通过rest_init()函数新建了内核init线程,
init_task 变量的定义
init_task 是内核中的第一个线程,贯穿于整个Linux系统的初始化过程。并且这个也是整个内核中唯一一个没有使用kernel_thread创建的内核态进程。
我们来分析这个变量的定义。
# init/init_task.c
/*
* Set up the first task table, touch at your own risk!. Base=0,
* limit=0x1fffff (=2MB)
*/
struct task_struct init_task{
.stack = init_stack, # init_stack 定义在链接脚本中
.mm = NULL,
.active_mm = &init_mm, # 虚拟地址 init_mm 定义在 mm/init-mm.c 中
};
EXPORT_SYMBOL(init_task);
struct thread_info init_thread_info __init_thread_info = INIT_THREAD_INFO(init_task);
/* Attach to the thread_info data structure for proper alignment */
#define __init_thread_info __attribute__((__section__(".data..init_thread_info")))
# ./arch/arm/include/asm/thread_info.h
#define INIT_THREAD_INFO(tsk) \
{ \
.task = &tsk, \
.flags = 0, \
.preempt_count = INIT_PREEMPT_COUNT, \
.addr_limit = KERNEL_DS, \
}
# 定义了 init_thread_info 变量
init_thread_info.task = &init_task; # 赋值
init_thread_info.addr_limit = KERNEL_DS #
# arch/arm/include/asm/uaccess.h
/*
* Note that this is actually 0x1,0000,0000
*/
#define KERNEL_DS 0x00000000
- 搜索链接脚本
我们可以观察到这里定义了一个init_thread_union变量。arch/arm/kernel/vmlinux.lds:
. = ALIGN((1 << 12)); .data : AT(ADDR(.data) - 0) { . = ALIGN(((1 << 12) << 1)); __start_init_task = .; init_thread_union = .; init_stack = .; KEEP(*(.data..init_task)) KEEP(*(.data..init_thread_info)) . = __start_init_task + ((1 << 12) << 1); __end_init_task = .; . = ALIGN((1 << 12)); __nosave_begin = .; *(.data..nosave) . = ALIGN((1 << 12)); __nosave_end = .; . = ALIGN((1 << 12)); *(.data..page_aligned) . = ALIGN((1 << 6)); *(.data..cacheline_aligned) . = ALIGN((1 << 6)); *(.data..read_mostly) . = ALIGN((1 << 6)); *(.xiptext) *(.data) *(.ref.data) *(.data..shared_aligned) *(.data.unlikely) __start_once = .; *(.data.once) __end_once = .; . = ALIGN(32); *(__tracepoints) . = ALIGN(8); __start___jump_table = .; KEEP(*(__jump_table)) __stop___jump_table = .; . = ALIGN(8); __start___verbose = .; KEEP(*(__verbose)) __stop___verbose = .; __start___trace_bprintk_fmt = .; KEEP(*(__trace_printk_fmt)) __stop___trace_bprintk_fmt = .; __start___tracepoint_str = .; KEEP(*(__tracepoint_str)) __stop___tracepoint_str = .; CONSTRUCTORS } . = ALIGN(8); __bug_table : AT(ADDR(__bug_table) - 0) { __start___bug_table = .; KEEP(*(__bug_table)) __stop___bug_table = .; }
并且我们可以观察到init_thread_info变量被 __init_thread_info 宏链接到了.data..init_thread_info 段。下面我们来分析 init_thread_union变量怎么被使用。__start_init_task = init_thread_union = init_stack
*(.data..init_task)
*(.data..init_thread_info)
. = __start_init_task + ((1 << 12) << 1) # __start_init_task + 0x80000 也就是说 上面两个段能够占据的长度是固定的。
SP指针的指定
__mmap_switched 怎么被调用的无需多言。
- 2.3在ARM,Thumb和ThumbEE状态之间切换
arch/arm/kernel/head-common.S
__mmap_switched:
...
adr r4, __mmap_switched_data # 将__mmap_switched_data 地址赋给 r4
mov fp, #0
# http://www.keil.com/support/man/docs/armasm/armasm_dom1359731126163.htm
# 下面的ARM 以及 THUMB表示当前处于什么指令集下面,就处理什么指令
# 我们只分析 arm 指令集的操作
# 1. R4 表示要更新R4
# 2. 将R4指向地址的数据全部取出来。
# 3. R0 = [R4] = [__mmap_switched_data], R4 = R4 + 4
# 4. R1 = [R4] = [__mmap_switched_data+4] , R4 = R4 + 4
# 5. sp = [R4] = [__mmap_switched_data+8] , R4 = R4 + 4
ARM( ldmia r4!, {r0, r1, sp} )
THUMB( ldmia r4!, {r0, r1, r3} )
THUMB( mov sp, r3 )
# 执行了上面的数据以后, SP = init_thread_union + THREAD_START_SP
# 这里SP指针就已经设置好了
# 下面是 BSS段的清楚,没啥好说的
sub r2, r1, r0
mov r1, #0
bl memset @ clear .bss
...
# 到这里就开始调用 start_kernel 了。
b start_kernel
__mmap_switched_data:
#ifdef CONFIG_XIP_KERNEL
#ifndef CONFIG_XIP_DEFLATED_DATA
.long _sdata @ r0
.long __data_loc @ r1
.long _edata_loc @ r2
#endif
.long __bss_stop @ sp (temporary stack in .bss)
#endif
.long __bss_start @ r0
.long __bss_stop @ r1
.long init_thread_union + THREAD_START_SP @ sp
.long processor_id @ r0
.long __machine_arch_type @ r1
.long __atags_pointer @ r2
上面我们分析了, SP = init_thread_union + THREAD_START_SP。并且在前文我们也已经知道了init_thread_union 变量的定义。下面我们来分析THREAD_START_SP变量的定义。 ```c / PAGE_SHIFT determines the page size /
define PAGE_SHIFT 12
define PAGE_SIZE (_AC(1,UL) << PAGE_SHIFT) # (1UL<<12) # 4096字节 .4K
define THREAD_SIZE_ORDER 1
define THREAD_SIZE (PAGE_SIZE << THREAD_SIZE_ORDER) # 4096 << 1 = 8192
define THREAD_START_SP (THREAD_SIZE - 8) # 8184 为什么这里减8 不是很理解,不过影响不大
现在我们知道SP 实际的指定范围了。
```c
SP = init_thread_union + 8184 .
现在我们理一下已经知道的内容。
- struct task_struct init_task 静态初始化的时候,已经指定了 .stack=init_stack .
在调用start_kernel 之前, 已经将SP指针设置好了为 SP = init_thread_union + 8184。 其中init_thread_union =init_stack 。
init_mm的定义
静态定义了一个struct mm_struct结构体init_mm变量。关于struct mm_struct结构体可以参考
- Linux数据结构
简单一句话就是用来描述一个进程或者任务的虚拟存储器。
# mm/init-mm.c:28:struct mm_struct init_mm = {
struct mm_struct init_mm = {
.mm_rb = RB_ROOT,
.pgd = swapper_pg_dir,
.mm_users = ATOMIC_INIT(2),
.mm_count = ATOMIC_INIT(1),
.mmap_sem = __RWSEM_INITIALIZER(init_mm.mmap_sem),
.page_table_lock = __SPIN_LOCK_UNLOCKED(init_mm.page_table_lock),
.arg_lock = __SPIN_LOCK_UNLOCKED(init_mm.arg_lock),
.mmlist = LIST_HEAD_INIT(init_mm.mmlist),
.user_ns = &init_user_ns,
.cpu_bitmap = { [BITS_TO_LONGS(NR_CPUS)] = 0},
INIT_MM_CONTEXT(init_mm)
};
上面。关于 idle 的栈,以及虚拟内存数据结构体都已经分配好了。
start_kernel 进行一系列的操作以后,将会调用rest_init函数。在rest_init函数开始以后,内核开始创建进程。
rest_init 函数
start_kernel 函数调用的最后一个函数就是rest_init。这个函数开始创建线程了
static void rest_init(void)
rcu_scheduler_starting();
# 内核线程实际上就是一个共享父进程地址空间的进程,它有自己的系统堆栈.
pid = kernel_thread(kernel_init, NULL, CLONE_FS); // 创建 PID = 1 的进程, kernel_init
pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES); // 创建PID = 2的进程, kthreadd
schedule_preempt_disabled()
sched_preempt_enable_no_resched();
schedule(); # 开始运行调度,
preempt_disable();
cpu_startup_entry(CPUHP_ONLINE);
arch_cpu_idle_prepare();
cpuhp_online_idle(state);
while (1)
do_idle(); # 循环调用do_idle
上面我们分析了rest_init函数创建了两个线程,然后循环调用do_idle函数,
do_idle函数的作用就不用分析了。
可以参考 1. What does an idle CPU process do?
简单理解就是,在Linux中,为每个处理器创建一个空闲任务,并将其锁定到该处理器。只要该CPU上没有其他进程可运行,就计划空闲任务。
1号内核进程
上面我们知道了kernel_init 进程被rest_init 函数创建。
kernel_init最开始只是一个函数,这个函数作为进程被启动,但是之后它将读取根文件系统下的init程序,这个操作将完成从内核态到用户态的转变,而这个init进程是所有用户态进程的父进程,它生了大量的子进程,所以init进程将永远存在,其PID是1
kernel_init
kernel_init_freeable
workqueue_init(); # 工作队列的初始化
if (ksys_open((const char __user *) "/dev/console", O_RDWR, 0) < 0) # 打开控制台(串口),不允许失败 ,标准输入
(void) ksys_dup(0); # 标准输出
(void) ksys_dup(0); # 标准错误
ksys_access(ramdisk_execute_command,0); # ramdisk_execute_command 缺省为 rdinit=/init
run_init_process(ramdisk_execute_command); # 执行初始化脚本,
if (!try_to_run_init_process("/sbin/init") ||
!try_to_run_init_process("/etc/init") ||
!try_to_run_init_process("/bin/init") ||
!try_to_run_init_process("/bin/sh")) # 著名的 init 进程
上面我们可以看到使用了ramdisk_execute_command字符指针。这个变量的赋值就需要分析。
rdinit_setup
static int __init rdinit_setup(char *str)
ramdisk_execute_command = str;
__setup("rdinit=", rdinit_setup);
下面我们来分析rdinit_setup函数怎么被调用的。
通过setup宏定义obs_kernel_param结构变量都被放入.init.setup段中,这样一来实际是使.init.setup段变成一张表,Kernel在处理每一个启动参数时,都会来查找这张表,与每一个数据项中的成员str进行比较,如果完全相同,就会调用该数据项的函数指针成员setup_func所指向的函数(该函数是在使用setup宏定义该变量时传入的函数参数),并将启动参数 如rdinit=后面的内容 传给该处理函数。
1号用户进程
如果文件系统是 busybox,那么这个init 1号用户进程一般就是linuxrc。关于linuxrc的执行过程需要去分析busybox源码。就不再这里分析了。可以参考下面的文章
重点看Systemd init 。其他的可以略过。
总结
- 调用 start_kernel 函数以前设置好了SP寄存器。 init_thread_union + 8184。
- 保存uboot传入的数据的地址。
- start_kernel 函数最后调用rest_init函数。
- 生成kernel_init进程。
- 生成kthreadd进程。
- schedule 切换控制
- 循环运行 do_idle 函数
- kernel_init进程
- 打开标准输出,标准输入,标准错误三个控制台。
- 解析启动参数
- 最后启动init进程。
linux 在rest_init函数中。生成了两个进程,自己本身也属于一个进程 idle. 因此Linux 启动时。就拥有三个最核心的进程在运行。
问题
关于设备树镜像的解析是放在哪里进行的了?
- 在调用start_kernel 函数, 调用reset_init 以前。
- 具体可以参考 U-boot&设备树&Linux
参考资料