20189220 余超《Linux内核原理与分析》第四周作业

时间:2021-11-19 23:38:27

构造一个简单的Linux系统MenuOS

第三章基础知识

  • 计算机的三大法宝:存储计算机,函数调用堆栈,中断。
  • 操作系统的两把宝剑:中断上下文,进程上下文。
  • Linux内核源码的目录结构:
    arch目录:arch目录是linux内核目录中比较重要的一个目录,因为arch目录中的代码可以使Linux内核支持不同的CPU和体系结构。
    block目录:存放Linux存储体系中关于块设备管理的代码。
    crypto目录:存放常见的加密算法的代码。
    drivers目录:驱动目录,里面分别存放了Linux内核支持的所有硬件设备的驱动源代码。
    fs目录:文件目录,里面列出了Linux支持的各种文件系统。
    include目录:头文件目录,存放公共的头文件。
    init目录:init是初始化的意思,存放Linux内核启动时的初始化的源代码。
    ipc目录:IPC就是进程间的通信,ipc目录里面是Linux支持的ipc的代码实现。
    kernel目录:kernel的意思是内核,就是Linux内核,这个文件夹存放内核本身需要的一些核心代码文件。
    mm目录:存放Linux的内存管理代码。

    构造一个简单的Linux内核

    1.使用实验楼虚拟机打开shell,构建Linux系统MenuOS
cd LinuxKernel/  
qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img  

20189220 余超《Linux内核原理与分析》第四周作业

2.用gdb跟踪调试Linux内核的启动

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

20189220 余超《Linux内核原理与分析》第四周作业

另开一个shell窗口,输入gdb,然后运行下面的代码

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

20189220 余超《Linux内核原理与分析》第四周作业

我在做这步实验的时候,出现了下面的问题
20189220 余超《Linux内核原理与分析》第四周作业
后面我发现是因为理解错了打开另一个shell这句话,我打开另一个终端了,所以里面没有那个文件,应该是直接在这个终端里面点击鼠标右键水平分割一个shell。

3.在 start_kernel 处设置断点,并用list命令查看代码
20189220 余超《Linux内核原理与分析》第四周作业

4.在 rest_init 处设置断点,并用list命令查看代码
20189220 余超《Linux内核原理与分析》第四周作业

内核启动的代码分析

1.我们首先看一下start_kernel()的源代码:

asmlinkage __visible void __init start_kernel(void)
501{
502 char *command_line;
503 char *after_dashes;
504
505 /*
506  * Need to run as early as possible, to initialize the
507  * lockdep hash:
508  */
509 lockdep_init();
510 set_task_stack_end_magic(&init_task);
511 smp_setup_processor_id();
512 debug_objects_early_init();
513
514 /*
515  * Set up the the initial canary ASAP:
516  */
517 boot_init_stack_canary();
518
519 cgroup_init_early();
520
521 local_irq_disable();
522 early_boot_irqs_disabled = true;

这里我们看到了一个void lockdep_init(void) 函数,lockdep是一个内核调试模块,用来检查内核互斥机制(尤其是自旋锁)潜在的死锁问题。
接下来是看到init_task,其在文件linux-3.18.6/init/init_task.c中定义如下:

struct task_struct init_task = INIT_TASK(init_task);

可见它其实就是一个task_struct,与用户进程的task_struct一样。相当于《Linux内核分析(二)》中的PCB结构体。
init_task中保存了一个进程的所有基本信息,如进程状态,栈起始地址,进程号pid等,其特殊之处在于它的pid=0,也就是通常所说的0号进程,0号进程就是我们这样通过手工创建出来的。也就是start_kernel()创建了0号进程。
0号进程的任务范围是从最早的汇编代码一直到start_kernel()的执行结束。

2.接下来看一下start_kernel()的源代码:

403 kernel_thread(kernel_init, NULL, CLONE_FS);
404 numa_default_policy();
405 pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);
406 rcu_read_lock();
407 kthreadd_task = find_task_by_pid_ns(pid, &init_pid_ns);
408 rcu_read_unlock();
409 complete(&kthreadd_done);
410
411 /*
412  * The boot idle thread must execute schedule()
413  * at least once to get things moving:
414  */
415 init_idle_bootup_task(current);
416 schedule_preempt_disabled();
417 /* Call into cpu_idle with preempt disabled */
418 cpu_startup_entry(CPUHP_ONLINE);
419}
420
421/* Check for early params. *

通过rest_init()新建kernel_init和kthreadd内核线程。调用kernel_thread()创建1号内核线程
在rest_init()函数中有这样一句话:
kernel_thread(kernel_init, NULL, CLONE_FS);
其中kernel_thread()的源码在文件linux-3.18.6/kernel/fork.c中定义,如下:

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);
}

这里相当于fork出了新进程来执行kernel_init()函数。
3.pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);创建PID为2的内核线程

483int kthreadd(void *unused)
484{
485 struct task_struct *tsk = current;
486
487 /* Setup a clean context for our children to inherit. */
488 set_task_comm(tsk, "kthreadd");
489 ignore_signals(tsk);
490 set_cpus_allowed_ptr(tsk, cpu_all_mask);
491 set_mems_allowed(node_states[N_MEMORY]);
492
493 current->flags |= PF_NOFREEZE;
494
495 for (;;) {
496     set_current_state(TASK_INTERRUPTIBLE);
497     if (list_empty(&kthread_create_list))
498         schedule();
499     __set_current_state(TASK_RUNNING);
500
501     spin_lock(&kthread_create_lock);
502     while (!list_empty(&kthread_create_list)) {
503         struct kthread_create_info *create;
504
505         create = list_entry(kthread_create_list.next,
506                     struct kthread_create_info, list);
507         list_del_init(&create->list);
508         spin_unlock(&kthread_create_lock);
509
510         create_kthread(create);
511
512         spin_lock(&kthread_create_lock);
513     }
514     spin_unlock(&kthread_create_lock);

kthreadd函数的任务是管理和调度其他内核线程 kernel_thread。for 循环中运行 kthread_create_list 全局链表中维护的 kthread, 在create_kthread()函数中,会调用 kernel_thread 来生成一个新的进程并被加入到此链表中,因此所有的内核线程都是直接或者间接的以 kthreadd 为父进程。

总结

1.Linux内核启动的一些流程
start_kernel( )函数完成了Linux内核的初始化工作。几乎每天内核部件都是用这个函数进行初始化的,我们只是说道了其中的一小部分:
1.调用sched_init()函数来初始化调度程序
2.调用build_all_zonelists()函数俩初始化内存管理
3.调用page_alloc_init()函数来初始化伙伴系统分配程序
4.调用trap_init()函数和init_IRQ()函数以初始化IDT
5.调用softing_init()函数初始化TASKLET_SOFTIRQ和HI_SOFTIRQ(软中断)
6.调用time_init()初始化系统日期时间
7.调用kmem_cache_init()函数初始化slab分配器(普通和高速缓存)
8.调用calibrate_delay()函数用于确定CPU时钟(延迟函数)
9.调用kernel_thread()函数为进程1创建内个线程,这个内核线程又会创建其他的内核线程并执行/sbin/init程序
在start_kernel()开始执行之后会显示linux版本,除此之外,在init程序和内核线程执行的最后阶段还会显示很多其他信息。最后,就会在控制台上出现熟悉的登陆提示,通知用户Linux内核已经启动正在运行。

2.内核的进程分析
在本实验中,我分析了Linux系统的启动过程。最初执行的进程即是0号进程init_task,它是被静态产生的,内存栈的位置固定,执行一些初始化的工作。一直到start_kernel开始调用执行sched_init(),0号进程被init_idle(current, smp_processor_id())进程初始化成为一个idle task,变成上一次实验中的进程一样的,通过一个while循环不断执行,只要运行栈里没有别的进程它就执行,循环中不断检测运行栈里是否有其他进程并通过schedule函数进行调度。
其中idle进程的产生为:idle是一个进程,其pid号为 0。其前身是系统创建的第一个进程,也是唯一一个没有通过fork()产生的进程。它在本实验中,具体是由init/main.c中start_kernel函数的set_task_stack_end_magic(&init_task)这一行开始实现的。其中的init_task就是手工创建的PCB,pid=0的进程,也就是最终的idle进程。
而1号进程的产生为:而到了kernel_thread(kernel_init, NULL, CLONE_FS);则通过fork()建立了pid=1的1号进程,也叫init进程,它是第一个用户态进程,它会继续完成剩下的初始化工作,成为系统中的其他所有进程的祖先。而创建了1号进程后,随着init_idle_bootup_task(current);等函数的调用,0号进程就演变成了idle进程。而idle进程就是当系统没有进程需要执行的时候来调度用的。所以start_kernel里、rest_init里创建了0号进程,该进程在系统初始化时候建立,并在系统运行过程中一直存在;而由0号进程,生成了1号进程,以及之后的许许多多的进程。最后进入了cpu_startup_entry。这个其实就是调用了cpu_idle。其实里面就是在while循环里调用了0号进程。