linux内核启动分析

Posted 小坚学Linux

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了linux内核启动分析相关的知识,希望对你有一定的参考价值。

文章目录

我们前面看了start_kernel前的汇编进行的初始化工作,现在开始看start_kernel函数了:

1. set_task_stack_end_magic

void set_task_stack_end_magic(struct task_struct *tsk)

	unsigned long *stackend;

	stackend = end_of_stack(tsk);//获取内核栈末尾地址
	*stackend = STACK_END_MAGIC;//设置为魔数,
	//在每次栈空间被使用时,内核都会检查该位置的值是否保持不变
	//如果发现变化,就会发出栈溢出的警报。

#define task_stack_end_corrupted(task) \\
		(*(end_of_stack(task)) != STACK_END_MAGIC)

set_task_stack_end_magic函数就做了一件事,获取内核栈的末尾地址,然后给这个地址赋值STACK_END_MAGIC。然后linux内核是使用task_stack_end_corrupted这个宏判断内核是否栈溢出的。linux内核会在cpu睡眠的时候检查当前栈是否发生溢出。如果开启了schedule debug则会在schedule的时候检查上一个进程的栈是否发生溢出。如果我们真的担心这个问题,最好的做法是在内核的启动参数,一般是grub中添加stacktrace,那么开启内核的函数调用堆栈追踪功能,同事会检查堆栈是否溢出。

2.smp_setup_processor_id

void __init smp_setup_processor_id(void)

	//读取MPIDR_EL1寄存器,并且只保留4个等级表示该core属于哪个cluse等信息
	u64 mpidr = read_cpuid_mpidr() & MPIDR_HWID_BITMASK;
	set_cpu_logical_map(0, mpidr);//把core信息写入__cpu_logical_map

	/*
	 * clear __my_cpu_offset on boot CPU to avoid hang caused by
	 * using percpu variable early, for example, lockdep will
	 * access percpu variable inside lock_release
	 */
	//tpidr_el1存放当前cpu的线程ID
	set_my_cpu_offset(0);//初始化0到对应cpu的tpidr_el1和tpidr_el2寄存器
	pr_info("Booting Linux on physical CPU 0x%010lx [0x%08x]\\n",
		(unsigned long)mpidr, read_cpuid_id());

smp_setup_processor_id函数主要做了两个事情:

  1. 读取MPIDR_EL1,把该寄存器的4个等级的aff提取出来,写入该cpu号为下标的__cpu_logical_map数组中。这4组aff值在每个core中是唯一的,加起来可以表示core在整个soc的位置。aff0表示一个core中的第几个线程,因为有可能使用超线程技术,一个core中有两个线程;aff1表示core在cluster中的第几个core;aff2表示这是在第几个cluster中。这个值是soc厂商写死的,用于表示core的信息。
  2. 初始化tpidr_el1和tpidr_el2寄存器为0。以前我一直以为它表示当前cpu处于0号进程,当时认为这个寄存器是用来存放当前cpu正在处理的进程的进程号的,加速cpu找到current这个task_struct结构体。 现在看了代码才知道,这个寄存器是存放每cpu的offs值的,用于快速找到每cpu变量所在的内存。

3. debug_objects_early_init

我们没有开启CONFIG_DEBUG_OBJECTS,是个空函数。

4. cgroup_init_early

int __init cgroup_init_early(void)

	static struct cgroup_fs_context __initdata ctx;
	struct cgroup_subsys *ss;
	int i;

	ctx.root = &cgrp_dfl_root;
	init_cgroup_root(&ctx);//注册和初始化根控制组
	cgrp_dfl_root.cgrp.self.flags |= CSS_NO_REF;

	//初始化init_task.cgroups,这个RCU指针结构体,看看他的定义就懂了
	//struct css_set __rcu		*cgroups;
	RCU_INIT_POINTER(init_task.cgroups, &init_css_set);

	//组装好基本的控制组框架
	for_each_subsys(ss, i) //遍历所有启用的cgroup子系统,他们存放在cgroup_subsys数组中
		WARN(!ss->css_alloc || !ss->css_free || ss->name || ss->id,
		     "invalid cgroup_subsys %d:%s css_alloc=%p css_free=%p id:name=%d:%s\\n",
		     i, cgroup_subsys_name[i], ss->css_alloc, ss->css_free,
		     ss->id, ss->name);
		WARN(strlen(cgroup_subsys_name[i]) > MAX_CGROUP_TYPE_NAMELEN,
		     "cgroup_subsys_name %s too long\\n", cgroup_subsys_name[i]);

		ss->id = i;//设置ID
		ss->name = cgroup_subsys_name[i];//设置cgroup名字
		if (!ss->legacy_name)
			ss->legacy_name = cgroup_subsys_name[i];//设置legacy_name

		if (ss->early_init)
			cgroup_init_subsys(ss, true);//cgroup的某一个子系统
	
	return 0;

我们cgroup开启了cpuset、cpu、cpuacct、io、memory、devices、perf_event、hugetlb、pids这几个子系统。
cgroup_init_early函数主要做了几件:

  1. 调用函数init_cgroup_root注册和初始化根控制组;
  2. 初始化init_task.cgroups,这个RCU类型的指针;
  3. 遍历所有启用的cgroup子系统,初始化他们,主要是使用cgroup_init_subsys函数初始化子系统。

4.1 init_cgroup_root

void init_cgroup_root(struct cgroup_fs_context *ctx)

	struct cgroup_root *root = ctx->root;
	struct cgroup *cgrp = &root->cgrp;

	INIT_LIST_HEAD(&root->root_list);//初始化cgroup_root的链表
	atomic_set(&root->nr_cgrps, 1);//设置cgroup的数量为1
	cgrp->root = root;
	init_cgroup_housekeeping(cgrp);//初始化根cgroup的后勤工作

	//初始化root的flags、release_agent_path、name、cgrp。
	root->flags = ctx->flags;
	if (ctx->release_agent)
		strscpy(root->release_agent_path, ctx->release_agent, PATH_MAX);
	if (ctx->name)
		strscpy(root->name, ctx->name, MAX_CGROUP_ROOT_NAMELEN);
	if (ctx->cpuset_clone_children)
		set_bit(CGRP_CPUSET_CLONE_CHILDREN, &root->cgrp.flags);

init_cgroup_root函数主要做了一下工作:

  1. 初始化cgroup_root的链表,设置cgroup的数量为1;
  2. 调用函数init_cgroup_housekeeping初始化根cgroup的后勤工作;
  3. 初始化root的flags、release_agent_path、name、cgrp。

4.1.1 init_cgroup_housekeeping

static void init_cgroup_housekeeping(struct cgroup *cgrp)

	struct cgroup_subsys *ss;
	int ssid;

	INIT_LIST_HEAD(&cgrp->self.sibling);//初始化存放兄弟cgroup的链表
	INIT_LIST_HEAD(&cgrp->self.children);//初始化存放子cgroup的链表
	INIT_LIST_HEAD(&cgrp->cset_links);//初始化指向cgrp_cset_links的列表
	INIT_LIST_HEAD(&cgrp->pidlists);//初始化存放进程pid的链表
	mutex_init(&cgrp->pidlist_mutex);//初始化保护pidlists的互斥锁
	//初始化cgrp的其他成员参数
	cgrp->self.cgroup = cgrp;
	cgrp->self.flags |= CSS_ONLINE;
	cgrp->dom_cgrp = cgrp;
	cgrp->max_descendants = INT_MAX;
	cgrp->max_depth = INT_MAX;
	INIT_LIST_HEAD(&cgrp->rstat_css_list);
	prev_cputime_init(&cgrp->prev_cputime);//空函数

	for_each_subsys(ss, ssid)
		INIT_LIST_HEAD(&cgrp->e_csets[ssid]);

	init_waitqueue_head(&cgrp->offline_waitq);
	//初始化工作队列,用于release的时候调用cgroup1_release_agent函数启动一个用户态程序
	INIT_WORK(&cgrp->release_agent_work, cgroup1_release_agent);

init_cgroup_housekeeping主要是做了以下工作:

  1. 初始化存放兄弟cgroup的链表self.sibling
  2. 初始化存放子cgroup的链表self.children
  3. 初始化指向cgrp_cset_links的列表cgrp->cset_links
  4. 初始化存放进程pid的链表cgrp->pidlists
  5. 初始化保护pidlists的互斥锁cgrp->pidlist_mutex
  6. 初始化cgrp的其他成员参数

4.2 cgroup_init_subsys

因为启动阶段,ss->early_init为0,cgroup_init_subsys没有执行,先不看。

5. local_irq_disable

#define local_irq_disable()				\\
	do 						\\
		bool was_disabled = raw_irqs_disabled();\\
		raw_local_irq_disable();		\\
		if (!was_disabled)			\\
			trace_hardirqs_off();		\\
	 while (0)

local_irq_disable这个宏主要做了3个工作:

  1. 调用函数raw_irqs_disabled读取irq的状态;
  2. 调用函数raw_local_irq_disable关中断;
  3. 判断步骤一获取到的状态是否关闭,如果还没有关闭,就执行trace_hardirqs_off函数进行trace。

5.1 raw_irqs_disabled

#define raw_irqs_disabled()		(arch_irqs_disabled())

static inline int arch_irqs_disabled(void)

	return arch_irqs_disabled_flags(arch_local_save_flags());


static inline unsigned long arch_local_save_flags(void)

	unsigned long flags;

	asm volatile(ALTERNATIVE(
		"mrs	%0, daif",
		__mrs_s("%0", SYS_ICC_PMR_EL1),
		ARM64_HAS_IRQ_PRIO_MASKING)
		: "=&r" (flags)
		:
		: "memory");

	return flags;


static inline int arch_irqs_disabled_flags(unsigned long flags)

	int res;

	asm volatile(ALTERNATIVE(
		"and	%w0, %w1, #" __stringify(PSR_I_BIT),
		"eor	%w0, %w1, #" __stringify(GIC_PRIO_IRQON),
		ARM64_HAS_IRQ_PRIO_MASKING)
		: "=&r" (res)
		: "r" ((int) flags)
		: "memory");

	return res;

raw_irqs_disabled主要做了:

  1. 调用函数arch_local_save_flags获取daif,其中表示了cpu的irq情况,
  2. 调用函数arch_irqs_disabled_flags对步骤一的返回值进行处理,只保留irq的bit。

5.2 raw_local_irq_disable

#define raw_local_irq_disable()		arch_local_irq_disable()

static inline void arch_local_irq_disable(void)

	if (system_has_prio_mask_debugging()) 
		u32 pmr = read_sysreg_s(SYS_ICC_PMR_EL1);

		WARN_ON_ONCE(pmr != GIC_PRIO_IRQON && pmr != GIC_PRIO_IRQOFF);
	

	asm volatile(ALTERNATIVE(
		"msr	daifset, #2		// arch_local_irq_disable",
		__msr_s(SYS_ICC_PMR_EL1, "%0"),
		ARM64_HAS_IRQ_PRIO_MASKING)
		:
		: "r" ((unsigned long) GIC_PRIO_IRQOFF)
		: "memory");

raw_local_irq_disable主要是执行了汇编 “msr daifset, #2” ,从而达到关闭irq。

5.3 trace_hardirqs_off

void trace_hardirqs_off(void)

	lockdep_hardirqs_off(CALLER_ADDR0);

	if (!this_cpu_read(tracing_irq_cpu)) 
		this_cpu_write(tracing_irq_cpu, 1);
		tracer_hardirqs_off(CALLER_ADDR0, CALLER_ADDR1);
		if (!in_nmi())
			trace_irq_disable_rcuidle(CALLER_ADDR0, CALLER_ADDR1);
	

这个函数追进去看不太懂,只知道这是ftrace的东西。

6. boot_cpu_init

void __init boot_cpu_init(void)

	int cpu = smp_processor_id();

	/* Mark the boot cpu "present", "online" etc for SMP and UP case */
	set_cpu_online(cpu, true);//记录当前cpu为online
	set_cpu_active(cpu, true);//记录当前cpu为active
	set_cpu_present(cpu, true);//记录当前cpu为present
	set_cpu_possible(cpu, true);//记录当前cpu为possible

#ifdef CONFIG_SMP
	__boot_cpu_id = cpu;//记录引导的cpu ID
#endif

boot_cpu_init主要是设置当前cpu的状态,这些状态保存在全局变量__cpu_online_mask、__cpu_active_mask、__cpu_present_mask、__cpu_possible_mask中。

7. page_address_init

void __init page_address_init(void)

	int i;

	for (i = 0; i < ARRAY_SIZE(page_address_htable); i++) 
		INIT_LIST_HEAD(&page_address_htable[i].lh);
		spin_lock_init(&page_address_htable[i].lock);
	


static struct page_address_slot 
	struct list_head lh;			/* List of page_address_maps */
	spinlock_t lock;			/* Protect this bucket's list */
 ____cacheline_aligned_in_smp page_address_htable[1<<PA_HASH_ORDER];

struct page_address_map 
	struct page *page;
	void *virtual;
	struct list_head list;
;

static struct page_address_map page_address_maps[LAST_PKMAP];

page_address_init函数只有一个作用就是初始化page_address_htable数组,这个数组是一个哈希桶,这个列表存放的是struct page_address_map结构体,看到这个结构体我么就知道这是一个page到虚拟地址的映射关系,也就是说我们经常挺熟的反向映射。

8. early_security_init

不太能看懂,后续补,先学习一下。

跟踪分析Linux内核的启动过程

潘俊洋

原创作品转载请注明出处
《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000

一.准备

搭建环境

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
cd ~/Work/ 
wget https://www.kernel.org/pub/linux/kernel/v3.x/linux-3.18.6.tar.xz 
xz -d linux-3.18.6.tar.xz 
tar -xvf linux-3.18.6.tar 
cd linux-3.18.6 
make i386_defconfig 
make  
cd ~/Work/ 
mkdir rootfs 
git clone  https://github.com/mengning/menu.git # 话说这里为什么用MenuOS 我个人觉得老师一来是节约编译时间 二来也可以做做广告
cd menu
sudo apt-get install libc6:i386 lib32stdc++6 # 这两行安装非常有必要
sudo apt-get install lib32readline-gplv2-dev # 在64bit的Ubuntu环境下不能编译这个MenuOS的roofs 需要这些包来支持 即使用了-m32
gcc -o init linktable.c menu.c test.c -m32 -static -lpthread 
cd ../rootfs
cp ../menu/init ./ 
find . | cpio -o -Hnewc |gzip -9 > ../rootfs.img 
cd ~/Work/ 
qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img
sudo apt-get install libncurses5-dev # 保证make menuconfig可用
make menuconfig
kernel hacking->
copile-time checks and compile options
[*] compile the kernel with debug info
qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img -s -S

然后打开另一个shell,执行下面的命令:

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

设置完断点后,可以使用c让内核继续进行加载,加载到第一个断点start_kernel时

 

二.分析

在执行start_kernel时,期初会对CPU、内存等各种硬件设备进行初始化,这期间涉及到非常多的不同内核模块的加载。

在start_kernel的最后一项初始化,就是有关内核进程管理的初始化了。一旦这一项初始化完成,内核就加载成功了。

my_start_kernel,插入这个函数之后,我们自己的内核通过PCB的进程管理单元来管理了我们依次创建的四个简单进程,并通过时间片轮转的方式进行了调度。那么在实际的linux内核代码中,rest_init()到底是干什么才使得我们需要在它之前执行my_start_kernel呢?原因就是rest_init实际上是linux内核初始化进程的函数。如果我们在它执行之前自行创建我们自己的进程,并且利用自己的调度算法来调度之后创建的进程,那么rest_init则永远不会被执行,因为在它执行之前,我们自己的进程已经在轮转调度不会结束了。
下面我们就来看看实际linux初始化进程的内核代码rest_init(删掉了不关心的部分):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void rest_init(void)
{
    int pid;
    ………………
    kernel_thread(kernel_init, NULL, CLONE_FS);
    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);
 
    init_idle_bootup_task(current);
    schedule_preempt_disabled();
    cpu_startup_entry(CPUHP_ONLINE);
}

在rest_init的代码中,kernel_thread,被定义在文件arch/x86/kernel/fork.c中,它的功能是用来fork一个内核线程。

1
2
3
4
pid_t kernel_thread(int (*fn)(void *), void *arg, unsigned long flags){
    return do_fork(flags|CLONE_VM|CLONE_UNTRACED, (unsigned long)fn,
        (unsigned long)arg, NULL, NULL);
}

上面的代码我们可以看到,kernel_thread实际上就是取fork一个线程。

在执行kernel_thread时,kernel_init作为将要执行的函数指针传入,进程ID会被置为1。所以在这里,kernel_init内核线程被创建,进程号为1。
在完成内核进程的创建后,会创建kthreadd内核线程,作用则是管理和调度其他的内核线程。

kernel_init既然是将要执行,我们就来看看kernel_init又会执行什么:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
static int kernel_init(void *unused)
{
    int ret;
    kernel_init_freeable();
    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);
    }
    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);
    }
    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.");
}

事实上,kernel_init会继续进行内核的最后一些初始化的工作,直到最后一行实际上整个内核的初始化工作就已经正式完成了。

注意,我们创建的进程ID实际上是从1开始的。其中在kernel_init中创建的是1号进程,在刚才的kthreadd中创建的是2号进程。

那么接下来,为了让系统能够运作起来,剩下的这三行代码完成了非常重要的工作,它完成了CPU对任务的调度初始化,让内核真正的开始进入用户主导的阶段:

1
2
3
init_idle_bootup_task(current);
schedule_preempt_disabled();
cpu_startup_entry(CPUHP_ONLINE);

首先,init_idle_bootup_tast()会初始化一个idle(闲置)进程,这个进程不做任何其他事情,只负责消耗时间片。
然后通过schedule_preempt_disabled来设置这个进程是不会被调度。因为CPU显然利用率越高越好,不可能让调度程序调度一个只消耗时间片的进程。
最后,cpu_startup_entry 就会使得CPU在idle这样一个循环内进行工作,不断往复,从不返回。

1
2
3
4
5
void cpu_startup_entry(enum cpuhp_state state)
{
    arch_cpu_idle_prepare();
    cpu_idle_loop();
}

自此,整个内核的启动过程就全部完成了。

三.实验过程

我们来逐步加载idel进程和1号进程。

通过上面的分析,我们注意到有下面几个比较重要的断点需要我们设置:
start_kernel, page_address_init, trap_init, mm_init, rest_init, kernel_init, kthreadd, init_idle_bootup_task, cpu_startup_entry

以上是关于linux内核启动分析的主要内容,如果未能解决你的问题,请参考以下文章

Linux运维面试题

Linux内核分析 实验三:跟踪分析Linux内核的启动过程

Linux内核分析之跟踪分析Linux内核的启动过程

Linux内核分析:Linux内核启动流程分析

20135239 益西拉姆 linux内核分析 跟踪分析Linux内核的启动过程

linux内核分析作业3:跟踪分析Linux内核的启动过程