您的位置:首页 > 运维架构 > Linux

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
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: