内核启动(二)
上一篇笔记,写到了start_kernel。但是start_kernel非常的大,需要找到一个结束点。这里将以init进程的启动为结束点。
又因为我的目的,是想查看驱动是怎么被加载的,那么也会涉及到驱动加载过程的学习。
init进程的启动
start_kernel较长,先列出如下:
start_kernel的解析,参考了:
https://www.cnblogs.com/cslunatic/archive/2013/05/11/3072811.html
asmlinkage void __init start_kernel(void)
{
char * command_line;
extern struct kernel_param __start___param[], __stop___param[];
//只有一个cpu时,什么也不做
//多个cpu时,返回启动的那个CPU id
smp_setup_processor_id();
//为了打印各种锁的状态,需要有个模块来保存相应的信息,因此该函数初始化之
lockdep_init();
//初始化一些调试对象
debug_objects_early_init();
//不明白的地方
boot_init_stack_canary();
//control groups的初始化
cgroup_init_early();
//关中断
local_irq_disable();
early_boot_irqs_off();
//初始化中断描述符(irq_desc)
early_init_irq_lock_class();
//获取大内核锁
lock_kernel();
//初始化时钟事件管理器
tick_init();
//为了管理cpu,需要使用相应的数据,初始化之
boot_cpu_init();
//主要是初始化高端内存的映射表
page_address_init();
printk(KERN_NOTICE "%s", linux_banner);
//对内核架构进行再次初始化
setup_arch(&command_line);
//这个函数主要作用是设置最开始的初始化任务属于init_mm内存。在ARM里,这个函数为空。
mm_init_owner(&init_mm, &init_task);
//保存命令行,以便后面可以使用。
setup_command_line(command_line);
//主要作用是设置最多有多少个nr_cpu_ids结构。
setup_nr_cpu_ids();
//设置SMP体系per-CPU使用的内存空间,同时拷贝初始化段里数据。
setup_per_cpu_areas();
//作用是为SMP系统里引导CPU进行准备工作。在ARM系统单核里是空函数。
smp_prepare_boot_cpu(); /* arch-specific boot-cpu hooks */
//初始化所有内存管理节点列表,以便后面进行内存管理初始化。
build_all_zonelists();
//设置内存页分配通知器
page_alloc_init();
printk(KERN_NOTICE "Kernel command line: %s\n", boot_command_line);
//分析命令行最早使用的参数
parse_early_param();
//内核参数进行解释,如果不能识别的命令就调用最后参数的函数
parse_args("Booting kernel", static_command_line, __start___param,
__stop___param - __start___param,
&unknown_bootoption);
//pid的hash初始化
pidhash_init();
//虚拟文件系统的缓存初始化
vfs_caches_init_early();
//对内核内部的异常表进行堆排序,以便加速访问
sort_main_extable();
//对异常进行初始化,在ARM系统里是空函数,没有任何的初始化
trap_init();
//内存的初始化,比如多少内存可用
mm_init();
//初始化进程调度器
sched_init();
//禁止抢占
preempt_disable();
//如果打开中断,就会提示,并把关闭中断
if (!irqs_disabled()) {
printk(KERN_WARNING "start_kernel(): bug: interrupts were "
"enabled *very* early, fixing it\n");
local_irq_disable();
}
//初始化RCU
rcu_init();
//初始化中断
early_irq_init();
init_IRQ();
//初始化优先树
prio_tree_init();
//初始化时钟相关
init_timers();
//初始化高精度时钟
hrtimers_init();
//软中断初始化
softirq_init();
//初始化系统时钟计时
timekeeping_init();
//初始化系统时钟
time_init();
//初始化profile工具使用的内存
profile_init();
//如果打开了中断,则打印提示
if (!irqs_disabled())
printk(KERN_CRIT "start_kernel(): bug: interrupts were "
"enabled early\n");
//设置一个变量,该变量用于标识内核启动的早期阶段
early_boot_irqs_on();
//打开中断
local_irq_enable();
/* Interrupts are enabled now so all GFP allocations are safe. */
set_gfp_allowed_mask(__GFP_BITS_MASK);
//高速缓存最后的初始化
kmem_cache_init_late();
/*
* HACK ALERT! This is early. We're enabling the console before
* we've done PCI setups etc, and console_init() must be aware of
* this. But we do want output early, in case something goes wrong.
*/
//控制台初始化
console_init();
//这段代码是判断分析输入的参数是否出错,如果有出错,就启动控制台输出之后,立即打印出错的参数,以便用户立即看到出错的地方
if (panic_later)
panic(panic_later, panic_param);
//打印锁的依赖信息,用来调试锁
lockdep_info();
//用来测试锁的API是否使用正常,进行自我测试
locking_selftest();
#ifdef CONFIG_BLK_DEV_INITRD
if (initrd_start && !initrd_below_start_ok &&
page_to_pfn(virt_to_page((void *)initrd_start)) < min_low_pfn) {
printk(KERN_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;
}
#endif
//容器组的页面内存分配
page_cgroup_init();
//内存分配的一些调试信息
enable_debug_pagealloc();
//内存调试先关
kmemtrace_init();
kmemleak_init();
debug_objects_mem_init();
//IDR机制的内存缓存
idr_init_cache();
//per-CPU的高速缓存设置
setup_per_cpu_pageset();
//初始化NUMA的内存访问策略
numa_policy_init();
//初始化时钟相关
if (late_time_init)
late_time_init();
//调度器相关时钟初始化
sched_clock_init();
//延迟的校准
calibrate_delay();
//pid map初始化
pidmap_init();
//反向映射的匿名内存
anon_vma_init();
//初始化EFI的接口,并进入虚拟模式
#ifdef CONFIG_X86
if (efi_enabled)
efi_enter_virtual_mode();
#endif
//线程信息的缓存
thread_info_cache_init();
//不明白
cred_init();
//fork环境初始化
fork_init(totalram_pages);
//进程缓存初始化
proc_caches_init();
//文件缓冲区初始化
buffer_init();
//初始化安全键管理列表和结构
key_init();
//初始化安全管理框架,以便提供访问文件/登录等权限
security_init();
//虚拟文件系统初始化
vfs_caches_init(totalram_pages);
//radix tree 初始化
radix_tree_init();
//信号队列初始化
signals_init();
/* rootfs populating might need page-writeback */
//不明白
page_writeback_init();
//初始化proc文件系统
#ifdef CONFIG_PROC_FS
proc_root_init();
#endif
//初始化进程控制组
cgroup_init();
//初始化初始化CPUSET
cpuset_init();
//初始化任务状态相关的缓存、队列和信号量
taskstats_init_early();
//初始化每个任务延时计数,当一个任务等CPU运行,或者等IO同步时,都需要计算等待时间
delayacct_init();
//检查CPU配置、FPU等是否非法使用不具备的功能
check_bugs();
//初始化ACPI电源管理。
acpi_early_init(); /* before LAPIC and SMP init */
sfi_init_late();
//初始化内核跟踪模块
ftrace_init();
/* Do the rest non-__init'ed, we're now alive */
//剩下的初始化流程,创建内核线程init,并运行
rest_init();
接下来是rest_init的初始化
该函数,主要参考了:https://blog.csdn.net/xichangbao/article/details/52938240
static noinline void __init_refok rest_init(void)
__releases(kernel_lock)
{
int pid;
//使能rcu机制
rcu_scheduler_starting();
//创建内核线程kernel_init
kernel_thread(kernel_init, NULL, CLONE_FS | CLONE_SIGHAND);
//设定NUMA系统的默认内存访问策略
numa_default_policy();
//创建内核线程kthreadd
pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);
//获取kthreadd的进程描述符
kthreadd_task = find_task_by_pid_ns(pid, &init_pid_ns);
unlock_kernel();
/*
* The boot idle thread must execute schedule()
* at least once to get things moving:
*/
//设置当前进程(0号进程)为idle进程类
init_idle_bootup_task(current);
preempt_enable_no_resched();
//主动调度,并禁止抢占
schedule();
preempt_disable();
/* Call into cpu_idle with preempt disabled */
//进入idle状态
cpu_idle();
}
该函数,主要功能是,创建两个内核线程,然后进入idle状态。kernel_init内核线程的启动入口在,kernel_init处,如下:
static int __init kernel_init(void * unused)
{
lock_kernel();
/*
* init can allocate pages on any node
*/
set_mems_allowed(node_possible_map);
/*
* 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);
//enable coherency
smp_prepare_cpus(setup_max_cpus);
//依次调用__initcall_start到__initcall_end之间的函数指针。
do_pre_smp_initcalls();
start_boot_trace();
//激活SMP系统中的其他CPU
smp_init();
sched_init_smp();
//此时与体系结构相关的部分已经初始化完
//现在初始化设备,完成外设及其驱动程序的加载和初始化
do_basic_setup();
/*
* 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函数中
init_post();
return 0;
}
init_post函数的内容如下:
参考了:https://www.cnblogs.com/gary-guo/p/6069149.html
static noinline int init_post(void)
__releases(kernel_lock)
{
/* need to finish all async __init code before freeing the memory */
async_synchronize_full();
//释放__init和__initdata标记的函数和数据使用的空间
free_initmem();
unlock_kernel();
//标记为只读
mark_rodata_ro();
system_state = SYSTEM_RUNNING;
numa_default_policy();
//打开终端
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);
//当前进程不可kill掉
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.
*/
//execute_command在init_setup()函数中被初始化,而后者会将命令行参数中"init="的参数值赋值给execute_command,因此,如果在命令行参数中设置了"init"参数,则执行该参数指定用户空间init进程
if (execute_command) {
run_init_process(execute_command);
printk(KERN_WARNING "Failed to execute %s. Attempting "
"defaults...\n", execute_command);
}
//如果没有,则分别尝试执行"/sbin/init","/etc/init","/bin/init","/bin/sh"
//否则就报错,为init进程未找到
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.");
}
上面启动的init进程,就是用标准c库写的,第一应用程序,进程编号为1。init进程负责初始化其他必要的进程。
驱动模块的加载
现在查看,被编译进内核中的驱动模块是如何被一步步加载执行的。即从start_kernel到各个模块的init函数的执行。
在上面kernel_init函数中,调用了do_basic_setup.函数如下
参考了:https://www.cnblogs.com/Oude/articles/12039103.html
static void __init do_basic_setup(void)
{
//初始化workqueue
init_workqueues();
//初始化cpuset
cpuset_init_smp();
//创建一个单线程工作队列khelper
usermodehelper_init();
//初始化tmpfs
init_tmpfs();
初始化驱动模型中的各子系统,可见的现象是在/sys中出现的目录和文件
driver_init();
//在proc文件系统中创建irq目录,并在其中初始化系统中所有中断对应的目录
init_irq_proc();
调用链接到内核中的所有构造函数,也就是链接进.ctors段中的所有函数
do_ctors();
//调用所有编译内核的驱动模块中的初始化函数。
do_initcalls();
}
上面函数的最后一个函数,就是调用所有的驱动模块的初始化函数。看看它是如何实现的。
static void __init do_initcalls(void)
{
initcall_t *call;
//__early_initcall_end和__initcall_end可在vmlinux.lds中查看
//可以看到中间定义的一系列段。只要在编译的时候,将这些初始化函数,
//编译到上述位置,就会被调用
for (call = __early_initcall_end; call < __initcall_end; call++)
//执行真正的调用
do_one_initcall(*call);
/* Make sure there is no pending stuff from the initcall sequence */
flush_scheduled_work();
}
linux中使用include/linux/init.h中的宏定义将相应的段,编译到指定的位置,如下:
#define __define_initcall(level,fn,id) \
static initcall_t __initcall_##fn##id __used \
__attribute__((__section__(".initcall" level ".init"))) = fn
#define pure_initcall(fn) __define_initcall("0",fn,0)
#define core_initcall(fn) __define_initcall("1",fn,1)
#define core_initcall_sync(fn) __define_initcall("1s",fn,1s)
#define postcore_initcall(fn) __define_initcall("2",fn,2)
#define postcore_initcall_sync(fn) __define_initcall("2s",fn,2s)
#define arch_initcall(fn) __define_initcall("3",fn,3)
#define arch_initcall_sync(fn) __define_initcall("3s",fn,3s)
#define subsys_initcall(fn) __define_initcall("4",fn,4)
#define subsys_initcall_sync(fn) __define_initcall("4s",fn,4s)
#define fs_initcall(fn) __define_initcall("5",fn,5)
#define fs_initcall_sync(fn) __define_initcall("5s",fn,5s)
#define rootfs_initcall(fn) __define_initcall("rootfs",fn,rootfs)
#define device_initcall(fn) __define_initcall("6",fn,6)
#define device_initcall_sync(fn) __define_initcall("6s",fn,6s)
#define late_initcall(fn) __define_initcall("7",fn,7)
#define late_initcall_sync(fn) __define_initcall("7s",fn,7s)
比如module_init宏,
#define module_init(x) __initcall(x);
#define __initcall(fn) device_initcall(fn)
#define device_initcall(fn) __define_initcall("6",fn,6)
最终会在.initcall6.init段中。因此在do_initcalls中可以被调用,并成功的初始化。
至此,终于看完了从uboot到内核启动,到驱动加载的整个流程。虽有很多不明白的东西,但是大体流程清楚了。接下来就是进入真正的驱动的学习了。
给自己一个鼓励。