简介

参考

  1. Linux引导过程的6个阶段(启动顺序)
  2. ARM LInux 启动过程分析
  3. linux的 0号进程 和 1 号进程
  4. 内核启动阶段kernel_init(init)进程分析

下面我们来分析典型得Linux启动过程。

首先。 我们需要直到三个重要得组件,

  • bootloader
  • kernel
  • rootfs.

首先bootloader引导这一块就无需讨论。直接分析kernel得操作。
下面是大致得启动流程。

image.png
image.png

0号进程

这个进程又可以叫做Idle进程。是唯一一个没有通过fork或者kernel_thread产生的进程。完成系统加载后,演变成为进程调度。

idle创建

init_task是内核中所有进程、线程的task_struct雏形,在内核初始化过程中,通过静态定义构造出了一个task_struct接口,取名为init_task,然后在内核初始化的后期,通过rest_init()函数新建了内核init线程,

init_task 变量的定义

init_task 是内核中的第一个线程,贯穿于整个Linux系统的初始化过程。并且这个也是整个内核中唯一一个没有使用kernel_thread创建的内核态进程。
我们来分析这个变量的定义。

  1. # init/init_task.c
  2. /*
  3. * Set up the first task table, touch at your own risk!. Base=0,
  4. * limit=0x1fffff (=2MB)
  5. */
  6. struct task_struct init_task{
  7. .stack = init_stack, # init_stack 定义在链接脚本中
  8. .mm = NULL,
  9. .active_mm = &init_mm, # 虚拟地址 init_mm 定义在 mm/init-mm.c 中
  10. };
  11. EXPORT_SYMBOL(init_task);
  12. struct thread_info init_thread_info __init_thread_info = INIT_THREAD_INFO(init_task);
  13. /* Attach to the thread_info data structure for proper alignment */
  14. #define __init_thread_info __attribute__((__section__(".data..init_thread_info")))
  1. # ./arch/arm/include/asm/thread_info.h
  2. #define INIT_THREAD_INFO(tsk) \
  3. { \
  4. .task = &tsk, \
  5. .flags = 0, \
  6. .preempt_count = INIT_PREEMPT_COUNT, \
  7. .addr_limit = KERNEL_DS, \
  8. }
  9. # 定义了 init_thread_info 变量
  10. init_thread_info.task = &init_task; # 赋值
  11. init_thread_info.addr_limit = KERNEL_DS #
  12. # arch/arm/include/asm/uaccess.h
  13. /*
  14. * Note that this is actually 0x1,0000,0000
  15. */
  16. #define KERNEL_DS 0x00000000
  • 搜索链接脚本
    1. arch/arm/kernel/vmlinux.lds:
    2. . = 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_union变量。
    1. __start_init_task = init_thread_union = init_stack
    2. *(.data..init_task)
    3. *(.data..init_thread_info)
    4. . = __start_init_task + ((1 << 12) << 1) # __start_init_task + 0x80000 也就是说 上面两个段能够占据的长度是固定的。
    并且我们可以观察到init_thread_info变量被 __init_thread_info 宏链接到了.data..init_thread_info 段。下面我们来分析 init_thread_union变量怎么被使用。

    SP指针的指定

    __mmap_switched 怎么被调用的无需多言。
  1. 2.3在ARM,Thumb和ThumbEE状态之间切换

    1. arch/arm/kernel/head-common.S
    2. __mmap_switched
    3. ...
    4. adr r4, __mmap_switched_data # 将__mmap_switched_data 地址赋给 r4
    5. mov fp, #0
    6. # http://www.keil.com/support/man/docs/armasm/armasm_dom1359731126163.htm
    7. # 下面的ARM 以及 THUMB表示当前处于什么指令集下面,就处理什么指令
    8. # 我们只分析 arm 指令集的操作
    9. # 1. R4 表示要更新R4
    10. # 2. 将R4指向地址的数据全部取出来。
    11. # 3. R0 = [R4] = [__mmap_switched_data], R4 = R4 + 4
    12. # 4. R1 = [R4] = [__mmap_switched_data+4] , R4 = R4 + 4
    13. # 5. sp = [R4] = [__mmap_switched_data+8] , R4 = R4 + 4
    14. ARM( ldmia r4!, {r0, r1, sp} )
    15. THUMB( ldmia r4!, {r0, r1, r3} )
    16. THUMB( mov sp, r3 )
    17. # 执行了上面的数据以后, SP = init_thread_union + THREAD_START_SP
    18. # 这里SP指针就已经设置好了
    19. # 下面是 BSS段的清楚,没啥好说的
    20. sub r2, r1, r0
    21. mov r1, #0
    22. bl memset @ clear .bss
    23. ...
    24. # 到这里就开始调用 start_kernel 了。
    25. b start_kernel
    26. __mmap_switched_data:
    27. #ifdef CONFIG_XIP_KERNEL
    28. #ifndef CONFIG_XIP_DEFLATED_DATA
    29. .long _sdata @ r0
    30. .long __data_loc @ r1
    31. .long _edata_loc @ r2
    32. #endif
    33. .long __bss_stop @ sp (temporary stack in .bss)
    34. #endif
    35. .long __bss_start @ r0
    36. .long __bss_stop @ r1
    37. .long init_thread_union + THREAD_START_SP @ sp
    38. .long processor_id @ r0
    39. .long __machine_arch_type @ r1
    40. .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 不是很理解,不过影响不大

  1. 现在我们知道SP 实际的指定范围了。
  2. ```c
  3. SP = init_thread_union + 8184 .

现在我们理一下已经知道的内容。

  1. struct task_struct init_task 静态初始化的时候,已经指定了 .stack=init_stack .
  2. 在调用start_kernel 之前, 已经将SP指针设置好了为 SP = init_thread_union + 8184。 其中init_thread_union =init_stack 。

    init_mm的定义

    静态定义了一个struct mm_struct结构体init_mm变量。关于struct mm_struct结构体可以参考

  3. 内存映射

  4. Linux数据结构

简单一句话就是用来描述一个进程或者任务的虚拟存储器。

  1. # mm/init-mm.c:28:struct mm_struct init_mm = {
  2. struct mm_struct init_mm = {
  3. .mm_rb = RB_ROOT,
  4. .pgd = swapper_pg_dir,
  5. .mm_users = ATOMIC_INIT(2),
  6. .mm_count = ATOMIC_INIT(1),
  7. .mmap_sem = __RWSEM_INITIALIZER(init_mm.mmap_sem),
  8. .page_table_lock = __SPIN_LOCK_UNLOCKED(init_mm.page_table_lock),
  9. .arg_lock = __SPIN_LOCK_UNLOCKED(init_mm.arg_lock),
  10. .mmlist = LIST_HEAD_INIT(init_mm.mmlist),
  11. .user_ns = &init_user_ns,
  12. .cpu_bitmap = { [BITS_TO_LONGS(NR_CPUS)] = 0},
  13. INIT_MM_CONTEXT(init_mm)
  14. };

上面。关于 idle 的栈,以及虚拟内存数据结构体都已经分配好了。
start_kernel 进行一系列的操作以后,将会调用rest_init函数。在rest_init函数开始以后,内核开始创建进程。

rest_init 函数

start_kernel 函数调用的最后一个函数就是rest_init。这个函数开始创建线程了

  1. static void rest_init(void)
  2. rcu_scheduler_starting();
  3. # 内核线程实际上就是一个共享父进程地址空间的进程,它有自己的系统堆栈.
  4. pid = kernel_thread(kernel_init, NULL, CLONE_FS); // 创建 PID = 1 的进程, kernel_init
  5. pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES); // 创建PID = 2的进程, kthreadd
  6. schedule_preempt_disabled()
  7. sched_preempt_enable_no_resched();
  8. schedule(); # 开始运行调度,
  9. preempt_disable();
  10. cpu_startup_entry(CPUHP_ONLINE);
  11. arch_cpu_idle_prepare();
  12. cpuhp_online_idle(state);
  13. while (1)
  14. 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

  1. kernel_init
  2. kernel_init_freeable
  3. workqueue_init(); # 工作队列的初始化
  4. if (ksys_open((const char __user *) "/dev/console", O_RDWR, 0) < 0) # 打开控制台(串口),不允许失败 ,标准输入
  5. (void) ksys_dup(0); # 标准输出
  6. (void) ksys_dup(0); # 标准错误
  7. ksys_access(ramdisk_execute_command,0); # ramdisk_execute_command 缺省为 rdinit=/init
  8. run_init_process(ramdisk_execute_command); # 执行初始化脚本,
  9. if (!try_to_run_init_process("/sbin/init") ||
  10. !try_to_run_init_process("/etc/init") ||
  11. !try_to_run_init_process("/bin/init") ||
  12. !try_to_run_init_process("/bin/sh")) # 著名的 init 进程

上面我们可以看到使用了ramdisk_execute_command字符指针。这个变量的赋值就需要分析。

rdinit_setup

  1. static int __init rdinit_setup(char *str)
  2. ramdisk_execute_command = str;
  3. __setup("rdinit=", rdinit_setup);

下面我们来分析rdinit_setup函数怎么被调用的。

  1. Linux的 __setup解析 — 命令行处理

通过setup宏定义obs_kernel_param结构变量都被放入.init.setup段中,这样一来实际是使.init.setup段变成一张表,Kernel在处理每一个启动参数时,都会来查找这张表,与每一个数据项中的成员str进行比较,如果完全相同,就会调用该数据项的函数指针成员setup_func所指向的函数(该函数是在使用setup宏定义该变量时传入的函数参数),并将启动参数 如rdinit=后面的内容 传给该处理函数。

1号用户进程

如果文件系统是 busybox,那么这个init 1号用户进程一般就是linuxrc。关于linuxrc的执行过程需要去分析busybox源码。就不再这里分析了。可以参考下面的文章

  1. Initial RAM Disk
  2. 什么是 Init 系统

重点看Systemd init 。其他的可以略过。

总结

  1. 调用 start_kernel 函数以前设置好了SP寄存器。 init_thread_union + 8184。
    1. 保存uboot传入的数据的地址。
  2. start_kernel 函数最后调用rest_init函数。
    1. 生成kernel_init进程。
    2. 生成kthreadd进程。
    3. schedule 切换控制
    4. 循环运行 do_idle 函数
  3. kernel_init进程
    1. 打开标准输出,标准输入,标准错误三个控制台。
    2. 解析启动参数
    3. 最后启动init进程。

linux 在rest_init函数中。生成了两个进程,自己本身也属于一个进程 idle. 因此Linux 启动时。就拥有三个最核心的进程在运行。

问题

关于设备树镜像的解析是放在哪里进行的了?

  1. 在调用start_kernel 函数, 调用reset_init 以前。
  2. 具体可以参考 U-boot&设备树&Linux

    参考资料