3、分析Linux内核的启动过程

姓名:周毅

原创作品转载请注明出处 

Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000


一、基础知识

1、计算机通电后,CPU开始从一个固定的地址加载代码并开始执行,这个地址就是BIOS的驱动程序所在的位置,于是BIOS的驱动开始执行,BIOS会初始化启动许多硬件(硬盘、网卡等等);

2、BIOS加载硬盘 MBR 中的 GRUB 后,启动过程就被GRUB2接管,GRUB2加载内核和initrd image,并启动内核;

3、内核接管整个系统后,加载/sbin/init并创建第一个用户态的进程,init进程开始调用一系列的脚本来创建很多子进程,这些子进程负责初始化整个系统;

4、start_kernel( )是linux内核启动的主函数,在init/main.c中定义,本实验主要也是分析start_kernel()函数的执行过程,从内核启动到第一个进程创建;

二、实验过程

  1. cd LinuxKernel/
  2. qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img

  • 使用gdb跟踪调试内核

  1. qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img -s -S # 关于-s和-S选项的说明:
  2. # -S freeze CPU at startup (use ’c’ to start execution)
  3. # -s shorthand for -gdb tcp::1234 若不想使用1234端口,则可以使用-gdb tcp:xxxx来取代-s选项


另开一个shell窗口

  1. gdb
  2. (gdb)file linux-3.18.6/vmlinux # 在gdb界面中targe remote之前加载符号表
  3. (gdb)target remote:1234 # 建立gdb和gdbserver之间的连接,按c 让qemu上的Linux继续运行
  4. (gdb)break start_kernel # 断点的设置可以在target remote之前,也可以在之后

执行start_kernel时,内核启动界面停在此处:


三、代码分析

下面分析start_kernel,主要初始化了一些必要工作,如0号进程init_task,锁,内存,时钟,中断,进程管理等等:

asmlinkage __visible void __init start_kernel(void)
{
	char *command_line;//命令行,用来存放bootloader传递过来的参数
	char *after_dashes;//这两个变量为地址指针,指向内核启动参数处理相关结构体在内存的位置,

	/*
	 * Need to run as early as possible, to initialize the
	 * lockdep hash:
	 */
	lockdep_init();//建立一个哈希表(hash tables),就是一个前后指向的指针结构体数组.函数的主要作用是初始化锁的状态跟踪模块。
	set_task_stack_end_magic(&init_task);//设置第一个进程init_task
	smp_setup_processor_id();//针对SMP处理器,用于获取当前CPU的硬件ID,如果不是多核,函数为空
	debug_objects_early_init();//初始化哈希桶(hash buckets)并将static object和pool object放入poll列表,这样堆栈就可以完全操作了

	/*
	 * Set up the the initial canary ASAP:
	 */
	boot_init_stack_canary();//初始化堆栈保护,防止栈溢出攻击的堆栈保护关键字

	cgroup_init_early();//在系统启动时初始化cgroups,同时初始化需要early_init的子系统

	local_irq_disable();//关闭当前CPU的所有中断响应,操作CPSR寄存器。
	early_boot_irqs_disabled = true;//系统中断关闭标志,当early_init完毕后,会恢复中断设置标志为false。

/*
 * Interrupts are still disabled. Do necessary setups, then
 * enable them
 */
	boot_cpu_init();//设置当前引导系统的CPU在物理上存在,在逻辑上可以使用,并且初始化准备好,即激活当前CPU
	page_address_init();//初始化高端内存的映射表
	pr_notice("%s", linux_banner);//输出各种信息
	//★很重要的一个函数★ arch/arm/kernel/setup.c内核架构相关初始化函数,是非常重要的一个初始化步骤。其中包含了处理器相关参数的初始化、内核启动参数(tagged list)的获取和前期处理、内存子系统的早期初始化
	setup_arch(&command_line);
	mm_init_cpumask(&init_mm);//初始化代表内核本身内存使用的管理结构体init_mm,init_mm定义了整个kernel的内存空间。
	setup_command_line(command_line);//对cmdline进行备份和保存
	setup_nr_cpu_ids();//设置最多有多少个nr_cpu_ids结构
	setup_per_cpu_areas();//为系统中每个CPU的per_cpu变量申请空间,同时拷贝初始化段里数据(.data.percpu)
	smp_prepare_boot_cpu();	/* arch-specific boot-cpu hooks为SMP系统里引导CPU(boot-cpu)进行准备工作。在ARM系统单核里是空函数 */

	build_all_zonelists(NULL, NULL);//设置内存管理相关的node(节点,每个CPU一个内存节点)和其中的zone(内存域,包含于节点中,如)数据结构,以完成内存管理子系统的初始化,并设置bootmem分配器
	page_alloc_init();//设置内存页分配通知器

	pr_notice("Kernel command line: %s\n", boot_command_line);//输出命令参数到显示终端
	parse_early_param();//解析cmdline中的启动参数
	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();

	/*
	 * These use large bootmem allocations and must precede
	 * kmem_cache_init()
	 */
	setup_log_buf(0);//使用bootmeme分配一个记录启动信息的缓冲区
	pidhash_init();//进程ID的HASH表初始化,用bootmem分配并初始化PID散列表,由PID分配器管理空闲和已指派的PID,这样可以提供通PID进行高效访问进程结构的信息。LINUX里共有四种类型的PID,因此就有四种HASH表相对应。
	vfs_caches_init_early();//前期虚拟文件系统(vfs)的缓存初始化
	sort_main_extable();//对内核异常表(exception table)按照异常向量号大小进行排序,以便加速访问
	trap_init();//对内核陷阱异常进行初始化,在ARM系统里是空函数,没有任何的初始化
	mm_init();//标记哪些内存可以使用,并且告诉系统有多少内存可以使用,当然是除了内核使用的内存以外

	/*
	 * Set up the scheduler prior starting any interrupts (such as the
	 * timer interrupt). Full topology setup happens at smp_init()
	 * time - but meanwhile we still have a functioning scheduler.
	 */
	sched_init();//对进程调度器的数据结构进行初始化,创建运行队列,设置当前任务的空线程,当前任务的调度策略为CFS调度器
	/*
	 * Disable preemption - early bootup scheduling is extremely
	 * fragile until we cpu_idle() for the first time.
	 */
	preempt_disable();//关闭优先级调度。由于每个进程任务都有优先级,目前系统还没有完全初始化,还不能打开优先级调度。
	if (WARN(!irqs_disabled(),
		 "Interrupts were enabled *very* early, fixing it\n"))
		local_irq_disable();
	idr_init_cache();// 为IDR机制分配缓存,主要是为 struct idr_layer结构体分配空间
	rcu_init();//初始化直接读拷贝更新的锁机制。
	context_tracking_init();
	radix_tree_init();//内核radis 树算法初始化
	/* init some links before init_ISA_irqs() */
	early_irq_init();//前期外部中断描述符初始化,主要初始化数据结构
	init_IRQ();//对应架构特定的中断初始化函数
	tick_init();
	rcu_init_nohz();
	init_timers();//初始化引导CPU的时钟相关的数据结构,注册时钟的回调函数,当时钟到达时可以回调时钟处理函数,最后初始化时钟软件中断处理
	hrtimers_init();//初始化高精度的定时器,并设置回调函数。
	softirq_init();//初始化软件中断,软件中断与硬件中断区别就是中断发生时,软件中断是使用线程来监视中断信号,而硬件中断是使用CPU硬件来监视中断。
	timekeeping_init();//初始化系统时钟计时,并且初始化内核里与时钟计时相关的变量。
	time_init();//初始化系统时钟。开启一个硬件定时器,开始产生系统时钟就是system_timer的初始化,arch/arm/mach-msm/board-*.c
	sched_clock_postinit();
	perf_event_init();//CPU性能监视机制初始化,此机制包括CPU同一时间执行指令数,cache miss数,分支预测失败次数等性能参数
	profile_init();//分配内核性能统计保存的内存,以便统计的性能变量可以保存到这里
	call_function_init();//初始化所有CPU的call_single_queue,同时注册CPU热插拔通知函数到CPU通知链中
	WARN(!irqs_disabled(), "Interrupts were enabled early\n");
	early_boot_irqs_disabled = false;
	local_irq_enable();

	kmem_cache_init_late();//这是内核内存缓存(slab分配器)的后期初始化,当初始化完成之后,就可以使用通用内存缓存了

	/*
	 * 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("Too many boot %s vars at `%s'", panic_later,
		      panic_param);

	lockdep_info();//打印锁的依赖信息,用来调试锁

	/*
	 * Need to run this when irqs are enabled, because it wants
	 * to self-test [hard/soft]-irqs on/off lock inversion bugs
	 * too:
	 */
	locking_selftest();//测试锁的API是否使用正常,进行自我测试。比如测试自旋锁、读写锁、一般信号量和读写信号量。

#ifdef CONFIG_BLK_DEV_INITRD  //检查initrd的位置是否符合要求
	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;
	}
#endif
	page_cgroup_init();//初始化容器组的页面内存分配,mem_cgroup是cgroup体系中提供的用于memory隔离的功能,
	debug_objects_mem_init();//这个函数是创建调试对象内存分配初始化,所以紧跟内存缓存初始化后面
	kmemleak_init();//内核内存泄漏检测机制初始化
	setup_per_cpu_pageset();//创建每个CPU的高速缓存集合数组并初始化,此前只有启动页组。因为每个CPU都不定时需要使用一些页面内存和释放页面内存,为了提高效率,就预先创建一些内存页面作为每个CPU的页面集合。
	numa_policy_init();//初始化NUMA的内存访问策略。
	if (late_time_init)//主要运行时钟相关后期的初始化功能。
		late_time_init();
	sched_clock_init();//对每个CPU进行系统进程调度时钟初始化
	calibrate_delay();//主要计算CPU需要校准的时间,这里说的时间是CPU执行时间。
	pidmap_init();//进程位图初始化,一般情况下使用一页来表示所有进程占用情况。
	anon_vma_init();//初始化反向映射的匿名内存,提供反向查找内存的结构指针位置,快速地回收内存。
	acpi_early_init();
#ifdef CONFIG_X86
	if (efi_enabled(EFI_RUNTIME_SERVICES))
		efi_enter_virtual_mode();
#endif
#ifdef CONFIG_X86_ESPFIX64
	/* Should be run before the first non-init thread is created */
	init_espfix_bsp();
#endif
	thread_info_cache_init();//线程信息(thread_info)的缓存初始化。
	cred_init();//任务信用系统初始化
	fork_init(totalram_pages);//根据当前物理内存计算出来可以创建进程(线程)的最大数量,并进行进程环境初始化,为task_struct分配空间。
	proc_caches_init();//进程缓存初始化,为进程初始化创建机制所需的其他数据结构申请空间
	buffer_init();//初始化文件系统的缓冲区,并计算最大可以使用的文件缓存。
	key_init();//初始化内核安全键管理列表和结构,内核密钥管理系统
	security_init();//初始化内核安全管理框架,以便提供访问文件/登录等权限。
	dbg_late_init();//内核调试系统后期初始化
	vfs_caches_init(totalram_pages);//虚拟文件系统进行缓存初始化,提高虚拟文件系统的访问速度
	signals_init();//初始化信号队列缓存。信号管理系统
	/* rootfs populating might need page-writeback */
	page_writeback_init();//页面写机制初始化
	proc_root_init();//初始化系统进程文件系统,主要提供内核与用户进行交互的平台,方便用户实时查看进程的信息。
	cgroup_init();//进程控制组正式初始化,主要用来为进程和其子程提供性能控制。比如限定这组进程的CPU使用率为20%
	cpuset_init();//初始化CPUSET,CPUSET主要为控制组提供CPU和内存节点的管理的结构
	taskstats_init_early();//任务状态早期初始化,为结构体获取高速缓存,并初始化互斥机制。任务状态主要向用户提供任务的状态信息
	delayacct_init();//任务延迟机制初始化,初始化每个任务延时计数。当一个任务等CPU运行,或者等IO同步时,都需要计算等待时间。

	check_bugs();//检查CPU配置、FPU等是否非法使用不具备的功能,检查CPU BUG,软件规避BUG

	sfi_init_late();// SFI 初始程序晚期设置函数

	if (efi_enabled(EFI_RUNTIME_SERVICES)) {
		efi_late_init();
		efi_free_boot_services();
	}

	ftrace_init();//功能跟踪调试机制初始化,初始化内核跟踪模块,ftrace的作用是帮助开发人员了解Linux 内核的运行时行为,以便进行故障调试或性能分析 function trace.

	/* Do the rest non-__init'ed, we're now alive */
	rest_init();//剩余的初始化,至此,内核已经开始工作了
}

init/init_task.c中静态初始化init_task进程:

#include <linux/init_task.h>
#include <linux/export.h>
#include <linux/mqueue.h>
#include <linux/sched.h>
#include <linux/sched/sysctl.h>
#include <linux/sched/rt.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/mm.h>

#include <asm/pgtable.h>
#include <asm/uaccess.h>

static struct signal_struct init_signals = INIT_SIGNALS(init_signals);
static struct sighand_struct init_sighand = INIT_SIGHAND(init_sighand);

/* Initial task structure */
struct task_struct init_task = INIT_TASK(init_task);//init_task在此初始化
EXPORT_SYMBOL(init_task);

/*
 * Initial thread structure. Alignment of this is handled by a special
 * linker map entry.
 */
union thread_union init_thread_union __init_task_data =
	{ INIT_THREAD_INFO(init_task) };

rest_init()调用kernel_init()初始化1号进程init,然后init_task变为dle进程:

static noinline void __init_refok rest_init(void)
{
	int pid;

	rcu_scheduler_starting();
	/*
	 * We need to spawn init first so that it obtains pid 1, however
	 * the init task will end up wanting to create kthreads, which, if
	 * we schedule it before we create kthreadd, will OOPS.
	 */
	kernel_thread(kernel_init, NULL, CLONE_FS);//创建一个内核线程,实际上就是内核进程,调用kernel_init()
	numa_default_policy();
	pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);
	rcu_read_lock();
	kthreadd_task = find_task_by_pid_ns(pid, &init_pid_ns);
	rcu_read_unlock();
	complete(&kthreadd_done);

	/*
	 * The boot idle thread must execute schedule()
	 * at least once to get things moving:
	 */
	init_idle_bootup_task(current);//init_task变为idle进程
	schedule_preempt_disabled();
	/* Call into cpu_idle with preempt disabled */
	cpu_startup_entry(CPUHP_ONLINE);
}

kernel_init()通过run_init_process()执行init:

static int __ref kernel_init(void *unused)
{
	int ret;

	kernel_init_freeable();
	/* need to finish all async __init code before freeing the memory */
	async_synchronize_full();
	free_initmem();
	mark_rodata_ro();
	system_state = SYSTEM_RUNNING;
	numa_default_policy();

	flush_delayed_fput();

	if (ramdisk_execute_command) {
		ret = run_init_process(ramdisk_execute_command);
		if (!ret)
			return 0;
		pr_err("Failed to execute %s (error %d)\n",
		       ramdisk_execute_command, ret);
	}

	/*
	 * 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.
	 */
	if (execute_command) {
		ret = run_init_process(execute_command);
		if (!ret)
			return 0;
		pr_err("Failed to execute %s (error %d).  Attempting defaults...\n",
			execute_command, ret);
	}
/*这里的 run_init_process 就是通过 execve()来运行 init 程序。这里首先运行“/sbin/init”,如果失败再运行“/etc/init”,然后是“/bin/init”,然后是“/bin/sh”(也就是说,init 可执行文件可以放在上面代码中寻找的 4 个目录中都可以),如果都失败,则可以通过在系统
启动时在添加的启动参数来指定 init,比如 init=/home/wzhou/init。 */
	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"))
		return 0;

	panic("No working init found.  Try passing init= option to kernel. "
	      "See Linux Documentation/init.txt for guidance.");
}

run_init_process调用do_execve函数通过系统调用执行init:

static int run_init_process(const char *init_filename)
{
	argv_init[0] = init_filename;
	return do_execve(getname_kernel(init_filename),//构造 sys_execve 系统调用
		(const char __user *const __user *)argv_init,
		(const char __user *const __user *)envp_init);
}
四、总结init_task、idle、init进程:

     task 0 的进程结构(task_struct init_task)由INIT_TASK宏静态定义。该结构体(init_task)在linux启动时被设置为current_task。当初始化到rest_init函数中时,通过kernel_thread函数启动第一个内核线程kernel_init。kernel_init再通过do_execve启动/sbin/init。这就是我们看到的init进程,进程号为1。初始化的最后linux调用scheule()整个系统就运行起来了,init_task则变为idle进程。




评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值