Linux启动过程简略分析-start_kernel部分代码阅读
2015-03-22 22:41
537 查看
Linux 启动过程分析
源代码和工具
在kernel.org上可以下载各个版本的Linux.概述
简单的说, 电脑加电启动后, 会进行自检(BIOS), 然后根据启动顺序, 依次检查可启动设备, 将找到的第一个可启动设备中特定区域内的内容(MBR), 拷贝到内存指定位置, 然后跳转到这个指定位置运行.到此, 软件系统才开始进行启动.软件系统启动分很多阶段, 但是在这个时候, 可以暂时分为两个阶段:加载阶段, 运行阶段.由于每代计算机都要兼容上一代计算机, 硬件无法把操作系统内核一次性全部加载到内存中, 而且, 内核也需要按照自己的需求, 将一些内容加载到特定内存位置, 所以, 在加载阶段, 计算机硬件加载的只是一个操作系统自己定制的加载器.这个加载器会收集一部分硬件信息, 为内核运行准备好环境, 然后将内核文件加载到指定的内存区域里, 然后在执行加载好的内核.有的操作系统会有两段加载过程, 第一段加载过程完成必要的初始化后加载第二段启动过程, 第二段加载过程就可以提供一个界面, 使用户能设置启动参数, 最后根据向内核传递的设置参数, 加载内核, 影响内核的工作方式.操作系统有很多依赖体系的部分. 比如体系对内存的管理, 启动流程, 设备通信方式等等.从遥控器到大型机, 有太多的体系结构了.没必要了解每个体系结构.所以, 重要的部分应该是独立于体系结构的部分和操作系统如何处理与体系结构通信的部分, 这样才能更快地理解内核, 移植内核.
在进行一系列初始化(这部分严重依赖体系, 所以在/arch/x86目录下)之后, 进入
start_kernel, 可以认为该部分对体系的依赖已经不是很大. 在这里, , 只提供了初始化的通用总体流程, 在流程中执行的各种操作, 会根据体系不同, 进行相应的条件编译. 从
start_kernel开始, 就是使用C语言对内核的各个部分进行初始化, 最后进入
rest_init, 将自己变为IDLE运行.在这个过程中要经过各种针对体系的初始化, 以支持操作系统的功能.
start_kernel包括对硬件的初始化, 获取体系规格, 设置体系工作方式, 还包括对内核本身的初始化, 各种数据结构的初始化, 各种机制的初始化等.由于某些依赖于硬件, 比如任务调度依赖时钟中断, 所以要先初始化一部分硬件. 但是初始化某些硬件的时候, 也需要某些信息才能进行, 比如多核处理器上的调度方法和单核CPU的调度算法有所不同, 所以在调度之前要获取硬件规格.加之系统部件之间的相互依赖, 所以初始化的过程很复杂.每个部分的初始化可能分为很多阶段.
调试
在linux代码目录下执行make menuconfig进行设置, 选择确保
kernel hacking -> Compile-time checks and compiler options -> Compile the kernel with debug info选中, 然后在linux目录下执行
qemu -kernel ./arch/x86/boot/bzImage -s -S, 在另一个终端中执行gdb, 然后输入
file ./linux-3.18.6/vmlinux,
target remote :1234,
b start_kernel,
layout split,
c, 之后就会停在
start_kernel入口出. 其中file命令是加载要调试的文件, target是连接调试目标, b是设置断点, layout设置gdb的显示布局, c是运行被调试的程序.
代码分析
start_kernel
在start_kernel之前, 就将task 0设置完毕. 由于对
init_task来说, 很多部分是已知的, 所以用静态变量来设置
struct task_struct init_task = INIT_TASK(init_task);(/init/init_task.c). 每个task记录了该task可以运行在哪个CPU上, 内存空间(mm_struct), 优先级, 父子任务关系等等很多信息, thread_info则记录了内存空间, 运行上下文等线程相关内容. 在
start_kernel的后续运行中, 就在这个地址空间之中. 接下来对lockdep, cgroup, irq, time等内容进行初始化, 包括体系的初始化, 也包括系统内部一些机制的初始化, 这里略过.
rest_init
在start_kernel最后部分, 调用了
rest_init函数, 从此,
start_kernel完成了他的任务, 即对内核的初始化功能. 在
rest_init中, 有两个调用值得注意, 一个是
kernel_thread(kernel_init, NULL, CLONE_FS);, 另一个是
pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);, 在这里,创建了两个线程, 分别执行
kernel_init和
kthreadd
kernel_thread实际上是对
do_fork的一种封装. 即创建内核线程. 即复制当前的任务, 然后运行被复制出来的任务, 复制过程调用
copy_process来完成. 在执行被复制的任务之前, 还有很多操作, 比如
wake_up_new_task就是激活调度. 唤醒之前被复制的任务
区别于
start_kernel的初始化内核,
kernel_init最终会执行
/sbin/init,
/etc/init,
/bin/init,
/bin/sh. 而
kthreadd是一个守护进程(不会被关闭), 对
kthread_create_list表内的请求进行处理. 如果没有请求,则进行任务调度, 执行其他作业. 所以他不可能成为用户态的init进程.
到此,整理下思路:
start_kernel为0号进程, 调用
kernel_thread执行
kernel_init, 这个就是1号进程, 调用
kernel_thread执行
kthreadd, 这个就是2号进程. 1号进程运行shell, 不断地待用户输入, 处理输入命令, 并如此反复下去. 而kthreadd是守护进程, 会通过请求, 通过
create_kthread来创建内核任务的线程. 显而易见,
kernel_init成了用户态, 执行用户的操作,
create_kthread保留在内核态, 执行系统线程创建任务. 正好, 两个权限, 一边一个, 可是, 0号进程去哪里了呢?
在创建了两个主要的进程后, 调用了
cpu_startup_entry,
cpu_idle_loop, 而在
cpu_idle_loop中, 系统会不断地循环如下操作, 如果不需要调度, 则让cpu进入闲置状态, 如果需要调度, 则把CPU让给其他进程/线程执行.
小结
第一次阅读Linux代码, 感觉很兴奋. 之前断断续续读过一点点ULK, LDD, 对于Linux源代码异常恐惧, 一来不想掉到各种硬件的初始化设置当中去, 二来Linux本身提供的机制太多无从下手. 这礼拜很忙, 周六看了一下午, 写的也很匆忙, 留下了尾巴, 不是函数内容没读, 就是子系统没弄明白.在找资料的时候, 找到不少有用的资源. 深深地感觉到, 阅读代码也许没有想象的那么难. 对Linux的研究已经非常深入了, 也有了非常多的资料. 不管这回MOOC课上的怎么样, 都希望能把这个继续下去.
附: start_kernel代码简易阅读
lockdep_init(); 死锁检测部分的初始化, 初始化了两个数组classhash_table和chainhash_table.通过对lockdep_initialized变量的检测, 确保只会被执行一次.set_task_stack_end_magic(); 对init_task的stack部分的顶部设置为STACK_END_MAGIC(0x57AC6E9D), 以此检测栈空间是否溢出.
smp_setup_processor_id(); 设置cpu的id, x86为空函数.
debug_objects_early_init(); 将静态对象连接到obj_pool中, 这样就能进行追踪了.
boot_init_stack_canary() (@)
cgroup_init_early(); 初始化cgroup, 具体参考芥子须弥的Linux Cgroups详解(一), 他也提供了文档可供下载, 非常感谢. 这个总得来, 说就让Linux作为虚拟机的操作系统时, 性能有所提升, 功能有所拓展.
local_irq_disable(); 关闭IRQ, 从这里开始不会处理外部设备的中断请求, 以防止中断对初始化过程造成不可预知的影响. 对应CLI, 或者push后popf等等效操作. IRQ涉及IRQ Balence, 可以参考系统技术非业余研究的深度剖析告诉你irqbalance有用吗?.
early_boot_irqs_disabled = true;
boot_cpu_init(); 对于SMP, 会有一个CPU作为boot CPU.获得这个CPU的mask并标志为online, active, present, possible.(@RMW), 其中值得研究的是CPU mask和CPU affinity, CPU亲和性(程序由哪个或哪些CPU运行).
page_address_init(); (@)
pr_notice(“%s”, linux_banner); 在内核日志中以
KERN_NOTICE输出Linux Release版本号, 编译者, 编译主机, 编译器, UTS版本号(UTS_VERSION由脚本生成, @具体意义), 内核日志系统参见Nice_Future的内核日志及printk结构浅析.
setup_arch(&command_line); 计算机体系决定的初始化部分.用efi_enabled将EFI初始化代码和BIOS初始化代码分开.这部分初始化并配置了很多设备和功能(SMP, ACPI , NX等).也包含了为调试而对硬件进行的初始化操作.对bootloader的参数进行分析生成command_line, 对后续从操作进行了做出准备.详细参考voice_shen的Linux启动中setup_arch分析
mm_init_cpumask(&init_mm); 将cpumask清零, (@原因)
setup_command_line(command_line); 此前, bootloader传递的参数是存放在系统映像的全局变量区域内, 将其从镜像中复制出去, 以备以后其他函数拓展和使用.
setup_nr_cpu_ids(); 获得cpu数量, 保存在nr_cpu_ids中.
setup_per_cpu_areas(); 配置每个cpu使用的内存范围.
smp_prepare_boot_cpu(); SMP体系决定的启动操作.
build_all_zonelists(NULL, NULL); 建立系统内存页区链表.(@)
page_alloc_init(); 内存分配通知. (@)
pr_notice(“Kernel command line: %s\n”, boot_command_line); 输出bootloader传递过来的参数, 此处还没有进行参数分析.
parse_early_param(); 在这里调用
parse_args解析具体的参数,
after_dashes = parse_args(“Booting kernel”, static_command_line, __start___param, __stop___param - __start___param, -1, -1, &unknown_bootoption); 获得’/’之后的内容
if (!IS_ERR_OR_NULL(after_dashes))
parse_args(“Setting init args”, after_dashes, NULL, 0, -1, -1, set_init_arg); 分析’/’之后的内容
jump_label_init(); (@)
setup_log_buf(0); (@)
pidhash_init(); pid哈希表初始化.
vfs_caches_init_early(); 虚拟文件系统的缓存初始化.
sort_main_extable(); 整理内核内置的异常处理表. (@机制)
trap_init(); 初始化陷阱门.中断门在进入中断时自动屏蔽中断, 而陷阱门没有此步骤.
mm_init(); 内存的初始化, 包括页表和虚拟地址.
sched_init();不论是SMP还是单个CPU, 这里对每个CPU进行初始化, 之后开始调度.初始化包含runqueue等
preempt_disable(); 在调用
cpu_idle之前禁止抢占
if (WARN(!irqs_disabled(), “Interrupts were enabled very early, fixing it\n”))
local_irq_disable(); 在开启调度之前, 不应该开启中断.
idr_init_cache(); 初始化IDR机制, 这是将指针和整数关联管理的机制, 参见wangbaolin719的linux idr机制
rcu_init(); RCU机制的初始化, 是一种进程间同步的机制, 参见IMB的Linux 2.6内核中新的锁机制–RCU
context_tracking_init(); 如果设置了CONFIG_CONTEXT_TRACKING_FORCE, 对每个CPU的上下文设置跟踪.
radix_tree_init(); 初始化基树, 基树是用来跟踪绑定到地址映射上的核心页,该radix树允许内存管理代码快速查找标识为dirty或writeback的页, 进一步信息Linux公社的Linux内核Radix Tree
early_irq_init(); 早期IRQ初始化, 用
alloc_desc分配描述符. 并记录在基树中
init_IRQ(); 设置每CPU的终端向量. 如果APIC处理了, 那么这个位置可以重复利用.
tick_init(); 初始化tick control. (@tick control)
rcu_init_nohz(); 初始化读-拷贝修改锁. 这里CPU未在并行运行.
init_timers(); 见下
hrtimers_init(); 时间管理——高精度时钟、动态时钟——实现(二)及SunnyBeiKe的init_timers()
softirq_init(); 软中断初始化, zhangskd的硬中断和软中断
此后的内容还未读完全
timekeeping_init();
time_init();
sched_clock_postinit();
perf_event_init();
profile_init();
call_function_init();
WARN(!irqs_disabled(), “Interrupts were enabled early\n”);
early_boot_irqs_disabled = false;
local_irq_enable();
kmem_cache_init_late();
console_init(); 在PCI初始化之前启用console.console此时应该意识到, 很多硬件无法使用.这么做是为了尽早输出错误信息
if (panic_later)
panic(“Too many boot %s vars at `%s’”, panic_later, panic_param);
lockdep_info(); 输出lockdep信息到日志系统.
locking_selftest();
if (initrd_start && !initrd_below_start_ok && page_to_pfn(virt_to_page((void *)initrd_start)) < min_low_pfn) {
pr_crit(“initrd overwritten (0x%08lx < 0x%08lx) - disabling it.\n”, page_to_pfn(virt_to_page((void *)initrd_start)), min_low_pfn);
initrd_start = 0;
}
page_cgroup_init();
debug_objects_mem_init();
kmemleak_init();
setup_per_cpu_pageset();
numa_policy_init();
if (late_time_init)
late_time_init();
sched_clock_init();
calibrate_delay();
pidmap_init();
anon_vma_init();
acpi_early_init();
if (efi_enabled(EFI_RUNTIME_SERVICES))
efi_enter_virtual_mode();
init_espfix_bsp();
thread_info_cache_init();
cred_init();
fork_init(totalram_pages);
proc_caches_init();
buffer_init();
key_init();
security_init();
dbg_late_init();
vfs_caches_init(totalram_pages);
signals_init(); 信号系统的初始化.
page_writeback_init(); 内存(脏)写回的初始化
proc_root_init(); 为/proc创建高速缓存
cgroup_init(); cgroup的初始化操作(之前有cgroup介绍)
cpuset_init(); 由于可能存在多核心CPU, 而且存在多道任务, 内存也因为私有, 共有而产生不同复用类型. 所以在这三个集合中, 如何分配彼此成为一个问题, 源代码内/Documentation/cgroups/cpusets.txt
taskstats_init_early();
delayacct_init();
check_bugs();
sfi_init_late();
if (efi_enabled(EFI_RUNTIME_SERVICES)) {
efi_late_init();
efi_free_boot_services();
}
ftrace_init(); 初始化内核跟踪模块,ftrace的作用是帮助开发人员了解Linux 内核的运行时行为,以便进行故障调试或性能分析。
rest_init(); 到此, start_kernel开始退化成IDEL
文添艺
原创作品转载请注明出处
《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000
相关文章推荐
- 分析Linux内核启动过程:从start_kernel到init
- 嵌入式ARM Linux kernel启动过程之浅尝辄止分析start_kernel函数
- 跟踪分析Linux内核的启动过程(start_kernel到init进程启动)
- 分析Linux内核启动过程:从start_kernel到init
- ARM Linux启动流程分析——start_kernel前启动阶段(汇编部分)
- Linux 启动代码 Start_kernel()函数分析
- Linux启动过程的内核代码分析
- arm linux kernel 从入口到start_kernel 的代码分析
- Linux启动过程中init/main.c中的start_kernel()函数中的lock_kernel()函数
- linux启动分析---C程序入口函数start_kernel
- Linux启动过程的C语言代码分析
- arm linux kernel 从入口到start_kernel 的代码分析
- 【转】arm linux 从入口到start_kernel 代码详细分析
- linux2.4启动分析(2)---内核解压缩过程 compress booting kernel
- ARM linux的启动部分源代码简略分析
- arm linux kernel 从入口到start_kernel 的代码分析
- arm linux 从入口到start_kernel 代码分析——head.S分析——1
- ARM linux kernel从入口到start_kernel代码分析 -- 只到machine type选中为止
- arm linux kernel 从入口到start_kernel 的代码分析
- 慢慢分析linux代码--启动部分