从0开始认识linux操作系统


一、Linux操作系统分析课程小结

本节是对孟宁老师和李春杰老师的课程《Linux操作系统分析》的总结内容,主要涉及对Linux操作系统的一些感悟。本人实在太菜了,之前也从来没有接触过Linux相关内容,所以我只能浅浅的从一个初学者的角度说一点我的收获。课程分为八个章节,目录如下
在这里插入图片描述
在第一节课中,主要学习了Linux的发展历史、学习了简单的Linux命令行,这一课让我第一次接触到了Linux,好像也没那么难,跟windows的cmd差不多嘛。
之前在其他课程中学习过堆栈,也学习过存储程序,但是并不知道我们写的代码是怎么一点点执行的,都仅仅是书本知识。在第二章中,通过一个实验任务,动手操作,深入理解了存储程序计算机和函数调用堆栈框架在执行过程中所起的作用,分析了操作系统中进程之间是如何进行切换的。通过对mykernel的源代码分析,体会进程在切换过程中操作系统对进程运行环境的管理。
第三章要分析Linux内核,这章对我来说可太难了,阅读这么复杂的源码还是头一次,说实话,很难看明白,由于代码内容很多,只挑了几个老师说的重点部分粗略看了一下,如果有时间还想再详细了解一下,通过这章的实验让我了解了Linux内核启动的过程,具体见本文第三节。
第四章是讲系统调用,系统调用是操作系统发展的过程中为了保证系统安全性和稳定性的一种重要机制,可以通过系统调用让操作系统内核为自己所用,既方便又安全。
第五章我了解了什么是进程,以及它的创建,进程的创建就是像细菌复制一样,一个分成两个,然后用复制自己创建新的,实际上是系统调用的过程。
第六章学习可执行程序工作原理,也就是我们自己写的代码如何变成可执行文件的过程,大致分为:预处理、编译、汇编、链接,还认识了一个心的系统调用execve。
最后,我们学习了进程调度过程中,进程上下文切换是如何发生的。进程的调度少不了进程的切换,关键操作是:切换地址空间、切换内核堆栈、切换内核控制流程,加上一些必要的寄存器保存和恢复。这里,除去地址空间的切换,其他操作要强调“内核”一词。这是因为,这些操作并非针对用户代码,切换完成后,也没有立即跑到next的用户空间中执行。用户上下文的保存和恢复是通过中断和异常机制,在内核态和用户态相互切换时才发生的。从这个意义上讲,切换地址空间才是本质上想要达到的“用户代码和数据的切换”,其余的切换不过是内核中不同的控制流程在“交接棒”而已。

二、mykernel实验

1. 什么是mykernel?

mykernel2.0是由孟宁老师建立的用于开发自己的操作系统内核的平台,它基于 x86-64/Linux Kernel 5.4.34. 您可以在这里找到mykernel的安装说明https://github.com/mengning/mykernel 。包括下载mykernel补丁、linux5.4.34内核代码、依赖库文件和qemu模拟器,给linux内核打上mykernel的补丁等等,ppt上已经有详细的命令,逐条运行即可,这里不再重复。

2.运行mykernel内核环境

打开QEMU虚拟机,运行已经预先编译好的mykernel内核环境,qemu运行截图如下:
在这里插入图片描述

可以看到my_start_kernel在持续打印,同时my_timer_handler也在周期性打印。

3.源码分析

mymain.c


void __init my_start_kernel(void)
 {
     int i = 0;
     while(1)
     {
         i++;
         if(i%100000 == 0)
             pr_notice("my_start_kernel here  %d ",i); 
    }  
}

my_start_kernel函数,完成系统的初始化操作,它是一个死循环,每当计数器等于整100000时打印一条信息。
myinterrupt.c


void my_timer_handler(void)
 {
        pr_notice(">>>>>>>>>>>>>>>>>my_timer_handlerhere<<<<<<<<<<<<<<<<<<");
 }
 

在myinterrupt.c文件中,有一个时钟中断处理会调用的函数:my_timer_handler。mykernel系统启动后,会调用my_start_kernel函数周期性的调用my_timer_handler函数,我们只要编写这两个函数,完成系统进程的初始化和进程的轮转调度,就可以写出一个简单的操作系统内核了。

4.简单时间片轮转多道程序内核

要完成一个简单的时间片轮转多道程序内核非常简单,只需要在mykernel的基础上稍加修改。在https://github.com/mengning/mykernel上下载mypcb.h;mymain.c;myinterrupt.c;把这三个文件复制替换到原来的mykernel文件夹里面,即要覆盖前文所述的mykernel文件夹下mymain.c和myinterrupt.c,并新增mypcb.h。编译、运行,系统从执行process2切换到process3,qemu如下图所示:
在这里插入图片描述

再来分析以下具体是如何实现的。
mymain.c 负责完成各个进程的初始化并且启动0号进程。

/*
 *  linux/mykernel/mymain.c
 *
 *  Kernel internal my_start_kernel
 *  Change IA32 to x86-64 arch, 2020/4/26
 *
 *  Copyright (C) 2013, 2020  Mengning
 *  
 */
#include <linux/types.h>
#include <linux/string.h>
#include <linux/ctype.h>
#include <linux/tty.h>
#include <linux/vmalloc.h>


#include "mypcb.h"

tPCB task[MAX_TASK_NUM];
tPCB * my_current_task = NULL;
volatile int my_need_sched = 0;

void my_process(void);


void __init my_start_kernel(void)
{
    int pid = 0;
    int i;
    /* Initialize process 0*/
    task[pid].pid = pid;
    task[pid].state = 0;/* -1 unrunnable, 0 runnable, >0 stopped */
    task[pid].task_entry = task[pid].thread.ip = (unsigned long)my_process;
    task[pid].thread.sp = (unsigned long)&task[pid].stack[KERNEL_STACK_SIZE-1];
    task[pid].next = &task[pid];
    /*fork more process */
    for(i=1;i<MAX_TASK_NUM;i++)
    {
        memcpy(&task[i],&task[0],sizeof(tPCB));
        task[i].pid = i;
	    task[i].thread.sp = (unsigned long)(&task[i].stack[KERNEL_STACK_SIZE-1]);
        task[i].next = task[i-1].next;
        task[i-1].next = &task[i];
    }
    /* start process 0 by task[0] */
    pid = 0;
    my_current_task = &task[pid];
	asm volatile(
    	"movq %1,%%rsp\n\t" 	/* set task[pid].thread.sp to rsp */
    	"pushq %1\n\t" 	        /* push rbp */
    	"pushq %0\n\t" 	        /* push task[pid].thread.ip */
    	"ret\n\t" 	            /* pop task[pid].thread.ip to rip */
    	: 
    	: "c" (task[pid].thread.ip),"d" (task[pid].thread.sp)	/* input c or d mean %ecx/%edx*/
	);
} 

int i = 0;

void my_process(void)
{    
    while(1)
    {
        i++;
        if(i%10000000 == 0)
        {
            printk(KERN_NOTICE "this is process %d -\n",my_current_task->pid);
            if(my_need_sched == 1)
            {
                my_need_sched = 0;
        	    my_schedule();
        	}
        	printk(KERN_NOTICE "this is process %d +\n",my_current_task->pid);
        }     
    }
}

my_start_kernel 是系统启动后,最先调用的函数,在这个函数里完成了0号进程的初始化和启动,并创建了其它的进程PCB,以方便后面的调度。在模拟系统里,每个进程的函数代码都是一样的,即 my_process 函数,my_process 在执行的时候,会打印出当前进程的 id,从而使得我们能够看到当前哪个进程正在执行。在 my_process 也会检查一个全局标志变量 my_need_sched,一旦发现其值为 1 ,就调用 my_schedule 完成进程的调度。

myinterrupt.c 负责完成时钟中断的处理及进程的切换。

/*
 *  linux/mykernel/myinterrupt.c
 *
 *  Kernel internal my_timer_handler
 *  Change IA32 to x86-64 arch, 2020/4/26
 *
 *  Copyright (C) 2013, 2020  Mengning
 *
 */
#include <linux/types.h>
#include <linux/string.h>
#include <linux/ctype.h>
#include <linux/tty.h>
#include <linux/vmalloc.h>

#include "mypcb.h"

extern tPCB task[MAX_TASK_NUM];
extern tPCB * my_current_task;
extern volatile int my_need_sched;
volatile int time_count = 0;

/*
 * Called by timer interrupt.
 * it runs in the name of current running process,
 * so it use kernel stack of current running process
 */
void my_timer_handler(void)
{
    if(time_count%1000 == 0 && my_need_sched != 1)
    {
        printk(KERN_NOTICE ">>>my_timer_handler here<<<\n");
        my_need_sched = 1;
    } 
    time_count ++ ;  
    return;  	
}

void my_schedule(void)
{
    tPCB * next;
    tPCB * prev;

    if(my_current_task == NULL 
        || my_current_task->next == NULL)
    {
    	return;
    }
    printk(KERN_NOTICE ">>>my_schedule<<<\n");
    /* schedule */
    next = my_current_task->next;
    prev = my_current_task;
    if(next->state == 0)/* -1 unrunnable, 0 runnable, >0 stopped */
    {        
    	my_current_task = next; 
    	printk(KERN_NOTICE ">>>switch %d to %d<<<\n",prev->pid,next->pid);  
    	/* switch to next process */
    	asm volatile(	
        	"pushq %%rbp\n\t" 	    /* save rbp of prev */
        	"movq %%rsp,%0\n\t" 	/* save rsp of prev */
        	"movq %2,%%rsp\n\t"     /* restore  rsp of next */
        	"movq $1f,%1\n\t"       /* save rip of prev */	
        	"pushq %3\n\t" 
        	"ret\n\t" 	            /* restore  rip of next */
        	"1:\t"                  /* next process start here */
        	"popq %%rbp\n\t"
        	: "=m" (prev->thread.sp),"=m" (prev->thread.ip)
        	: "m" (next->thread.sp),"m" (next->thread.ip)
    	); 
    }  
    return;	
}

my_timer_handler 函数会被内核周期性的调用,每调用1000次,就去将全局变量my_need_sched的值修改为1,通知正在执行的进程执行调度程序my_schedule。在my_schedule函数中,完成进程的切换。进程的切换分两种情况,一种情况是下一个进程没有被调度过,另外一种情况是下一个进程被调度过,可以通过下一个进程的state知道其状态。进程切换依然是通过内联汇编代码实现,无非是保存旧进程的eip和堆栈,将新进程的eip和堆栈的值存入对应的寄存器中。

mypcb.h 负责完成进程控制块PCB结构体的定义。

/*
 *  linux/mykernel/mypcb.h
 *
 *  Kernel internal PCB types
 *
 *  Copyright (C) 2013  Mengning
 *
 */

#define MAX_TASK_NUM        4
#define KERNEL_STACK_SIZE   1024*2
/* CPU-specific state of this task */
struct Thread {
    unsigned long		ip;
    unsigned long		sp;
};

typedef struct PCB{
    int pid;
    volatile long state;	/* -1 unrunnable, 0 runnable, >0 stopped */
    unsigned long stack[KERNEL_STACK_SIZE];
    /* CPU-specific state of this task */
    struct Thread thread;
    unsigned long	task_entry;
    struct PCB *next;
}tPCB;

void my_schedule(void);

在这个文件里,定义了 Thread 结构体,用于存储当前进程中正在执行的线程的ip和sp,还有一个函数的声明 my_schedule,它的实现在my_interrupt.c中,在mymain.c中的各个进程函数会根据一个全局变量的状态来决定是否调用它,从而实现主动调度。

三、debug linux kernel实验

1.实验目标

构造一个Linux系统并跟踪分析Linux内核启动过程

2.实验过程

首先需要构造一个Linux系统,可以分为两步:
1.编译和运行内核
2. 制作根文件系统
在这里插入图片描述

具体操作方法可以参考孟宁老师ppt(https://gitee.com/mengning997/linuxkernel),成功编译内核之后qemu界面如下:
在这里插入图片描述

3.启动过程分析

有了基本的实验环境之后,就可以进行调试了,在start_kernel处设置断点,执行c调试,冻结的系统开始启动,断点执行到start _kernel,利用list命令查看,发现几乎全部重要的内核模块初始化都在start _kernel完成,这是内核启动的关键。当计算机系统加电(Power on PC)后,CPU由CS:EIP = FFFF:0000H处取指令,BIOS代码被调用执行。BIOS例行程序检测完硬件并完成相应的初始化之后就会寻找可引导介质,找到后把引导程序加载到指定内存区域后,就把控制权交给了引导程序BootLoader。BootLoader指定kernel、initrd和root所在的分区和目录来启动操作系统。Linux内核初始化在平台相关的汇编代码执行完毕后会跳转到start_kernel()函数,开始c代码的内核初始化。其中,内核人为制造出来(而不是其他进程通过do_fork)init_task(0号进程,该进程完全在内核态),即最终的idle进程。因为init_task是静态制造出来的,pid=0,它试图将从最早的汇编代码一直到start_kernel的执行都纳入到init_task进程上下文中。在start_kernel函数的最后,调用rest_init()函数来创建了init进程,即1号进程,以及kthreadd进程,即2号进程,系统开始正式对外工作了。从rest_init开始,Linux开始产生进程,在rest_init函数中,内核将通过kernel_init调用kernel_execve来执行根文件系统下的/sbin/init文件(所以此前系统根文件系统必须已经就绪),kernel_execve对用户空间程序/sbin/init的调用发起自int $0x80,这是个从内核空间发起的系统调用,与call_usermodehelper函数本质上是完全一样的。由此产生第一个真正的进程(pid=1)。
start_kernel在init/main.c中,随代码注释,如下所示:

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(); /*lockdep是一个内核调试模块,用来检查内核互斥机制(尤其是自旋锁)潜在的死锁问题。建立一个哈希表(hash tables),就是一个前后指向的指针结构体数组。 (函数的主要作用是初始化锁的状态跟踪模块。由于内核大量使用锁来进行多进程多处理器的同步操作,死锁就会在代码不合理的时候出现,但是要定位哪个锁比较困难,用哈希表可以跟踪锁的使用状态。死锁情况:一个进程递归加锁同一把锁;同一把锁在两次中断中加锁;几把锁形成闭环死锁)*/
    set_task_stack_end_magic(&init_task); /*init_task即手工创建的PCB,0号进程即最终的idle进程*/
    smp_setup_processor_id(); /*针对SMP处理器,用于获取当前CPU的硬件ID,如果不是多核,函数为空 (判断是否定义了CONFIG_SMP,如果定义了调用read_cpuid_mpidr读取寄存器CPUID_MPIDR的值,就是当前正在执行初始化的CPU ID,为了在初始化时做个区分,初始化完成后,所有处理器都是平等的,没有主从)*/
    debug_objects_early_init(); /*初始化哈希桶(hash buckets)并将static object和pool object放入poll列表,这样堆栈就可以完全操作了 (这个函数的主要作用就是对调试对象进行早期的初始化,就是HASH锁和静态对象池进行初始化,执行完后,object tracker已经开始完全运作了)*/

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

    cgroup_init_early(); /*在系统启动时初始化cgroups,同时初始化需要early_init的子系统 (这个函数作用是控制组(control groups)早期的初始化,控制组就是定义一组进程具有相同资源的占有程度,比如,可以指定一组进程使用CPU为30%,磁盘IO为40%,网络带宽为50%。目的就是为了把所有进程分配不同的资源)*/

    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 (在多CPU的系统里,内核需要管理多个CPU,那么就需要知道系统有多少个CPU,在内核里使用cpu_present_map位图表达有多少个CPU,每一位表示一个CPU的存在。如果是单个CPU,就是第0位设置为1。虽然系统里有多个CPU存在,但是每个CPU不一定可以使用,或者没有初始化,在内核使用cpu_online_map位图来表示那些CPU可以运行内核代码和接受中断处理。随着移动系统的节能需求,需要对CPU进行节能处理,比如有多个CPU运行时可以提高性能,但花费太多电能,导致电池不耐用,需要减少运行的CPU个数,或者只需要一个CPU运行。这样内核又引入了一个cpu_possible_map位图,表示最多可以使用多少个CPU。在本函数里就是依次设置这三个位图的标志,让引导的CPU物理上存在,已经初始化好,最少需要运行的CPU。)*/
    page_address_init();  /*初始化高端内存的映射表 (在这里引入了高端内存的概念,那么什么叫做高端内存呢?为什么要使用高端内存呢?其实高端内存是相对于低端内存而存在的,那么先要理解一下低端内存了。在32位的系统里,最多能访问的总内存是4G,其中3G空间给应用程序,而内核只占用1G的空间。因此,内核能映射的内存空间,只有1G大小,但实际上比这个还要小一些,大概是896M,另外128M空间是用来映射高端内存使用的。因此0到896M的内存空间,就叫做低端内存,而高于896M的内存,就叫高端内存了。如果系统是64位系统,当然就没未必要有高端内存存在了,因为64位有足够多的地址空间给内核使用,访问的内存可以达到10G都没有问题。在32位系统里,内核为了访问超过1G的物理内存空间,需要使用高端内存映射表。比如当内核需要读取1G的缓存数据时,就需要分配高端内存来使用,这样才可以管理起来。使用高端内存之后,32位的系统也可以访问达到64G内存。在移动操作系统里,目前还没有这个必要,最多才1G多内存)*/
    pr_notice("%s", linux_banner); /*输出各种信息(Linux_banner是在kernel/init/version.c中定义的,这个字符串是编译脚本自动生成的)*/
    setup_arch(&command_line); /*很重要的一个函数arch/arm/kernel/setup.c
(内核架构相关初始化函数,是非常重要的一个初始化步骤。其中包含了处理器相关参数的初始化、内核启动参数(tagged list)的获取和前期处理、内存子系统的早期初始化(bootmem分配器))*/
    mm_init_cpumask(&init_mm); /*每一个任务都有一个mm_struct结构来管理内存空间,init_mm是内核的mm_struct*/
    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系统里是空函数,没有任何的初始化。内有set_system_trap_gate(SYSCALL_VECTOR, &SYSTEM_CALL)*/
    mm_init(); 
/*标记哪些内存可以使用,并且告诉系统有多少内存可以使用,当然是除了内核使用的内存以外
(初始化内核内存分配器,包括六个子函数
1、page_cgroup_init_flatmem();获取page_cgroup所需内存
2、mem_init;关闭并释放bootmem分配器,打印内存信息,内核启动时看到Virtual kernel memory layout:的信息就是这个函数的
 
3、kmem_cache_init();初始化slab分配器
4、percpu_init_late();PerCPU变量系统后期初始化
5、pgtable_cache_init();也表缓存初始化,arm中是个空函数 6、vmalloc_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机制分配缓存,主要是为 structidr_layer结构体分配空间*/
    rcu_init(); /*初始化直接读拷贝更新的锁机制。 Read-Copy Update (RCU主要提供在读取数据机会比较多,但更新比较的少的场合,这样减少读取数据锁的性能低下的问题。)*/
    context_tracking_init();
    radix_tree_init(); /*内核radis 树算法初始化*/
    /* init some links before init_ISA_irqs() */
    early_irq_init(); /*前期外部中断描述符初始化,主要初始化数据结构*/
    init_IRQ(); /*对应架构特定的中断初始化函数,在ARM中就是machine_desc->init_irq(),就是运行设备描述结构体中的init_irq函数[arch/arm/mach-msm/board-xxx.c]*/
    tick_init(); /*初始化内核时钟系统,tick control,调用clockevents_register_notifier,就是监听时钟变化事件 (这个函数主要作用是初始化时钟事件管理器的回调函数,比如当时钟设备添加时处理。在内核里定义了时钟事件管理器,主要用来管理所有需要周期性地执行任务的设备)*/
    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"); /*对应前面的local_irq_disable() (打开本CPU的中断,也即允许本CPU处理中断事件,在这里打开引CPU的中断处理。如果有多核心,别的CPU还没有打开中断处理。)*/
    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
    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 /*检查initrd的位置是否符合要求,也就是判断传递进来initrd_start对应的物理地址是否正常,如果有误就打印错误信息,并清零initrd_start。*/
    page_cgroup_init(); /*初始化容器组的页面内存分配,mem_cgroup是cgroup体系中提供的用于memory隔离的功能,*/
    debug_objects_mem_init(); /*这个函数是创建调试对象内存分配初始化,所以紧跟内存缓存初始化后面*/
    kmemleak_init(); /*内核内存泄漏检测机制初始化;*/
    setup_per_cpu_pageset(); /*创建每个CPU的高速缓存集合数组并初始化,此前只有启动页组。因为每个CPU都不定时需要使用一些页面内存和释放页面内存,为了提高效率,就预先创建一些内存页面作为每个CPU的页面集合。*/
    numa_policy_init(); /*初始化NUMA的内存访问策略。所谓NUMA,它是NonUniform Memory AccessAchitecture(非一致性内存访问)的缩写,主要用来提高多个CPU访问内存的速度。因为多个CPU访问同一个节点的内存速度远远比访问多个节点的速度来得快。*/
    if (late_time_init)
        late_time_init(); /*主要运行时钟相关后期的初始化功能。*/
    sched_clock_init(); /*对每个CPU进行系统进程调度时钟初始化*/
    calibrate_delay(); /*主要计算CPU需要校准的时间,这里说的时间是CPU执行时间。如果是引导CPU,这个函数计算出来的校准时间是不需要使用的,主要使用在非引导CPU上,因为非引导CPU执行的频率不一样,导致时间计算不准确。BogoMIPS值,也是衡量cpu性能的标志*/
    pidmap_init(); /*进程位图初始化,一般情况下使用一页来表示所有进程占用情况。*/
    anon_vma_init(); /*初始化反向映射的匿名内存,提供反向查找内存的结构指针位置,快速地回收内存。*/
    acpi_early_init(); /*这个函数是初始化ACPI电源管理。高级配置及电源接口(Advanced Configuration and Power Interface)ACPI规范介绍ACPI能使软、硬件、操作系统(OS),主机板和外围设备,依照一定的方式管理用电情况,系统硬件产生的Hot-Plug事件,让操作系统从用户的角度上直接支配即插即用设备,不同于以往直接通过基于BIOS 的方式的管理。*/
#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 /*初始化EFI的接口,并进入虚拟模式。EFI是ExtensibleFirmware Interface的缩写,就是INTEL公司新开发的BIOS接口。*/
    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(); /*剩余的初始化,至此,内核已经开始工作了。在rest_init()中,有kernel_init(),通过调用run_init_process,产生了第一个用户态进程,1号进程,默认在根目录下。rest_init()中,cpu_starup_entry函数调用cpu_idle_loop使得init_task空闲为idle进程,即0号进程*/
}

从rest_init开始,Linux开始产生进程,因为init_task是静态制造出来的,pid=0,它试图将从最早的汇编代码一直到start_kernel的执行都纳入到init_task进程上下文中。在rest_init函数中,内核将通过下面的代码产生第一个真正的进程(pid=1)。rest_init()函数代码如下所示:

static noinline void __init_refok rest_init(void)
{
    int pid; /*定义pid变量存放进程号*/

    rcu_scheduler_starting(); /*RCU(Read-Copy Update)锁机制启动。*/
    /*
     * 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); /*init进程在此时创建好了,但是现在还不能调度它。*/
    numa_default_policy(); /*设定NUMA(Non-Uniform Memory Access Architecture)系统的内存访问策略为默认。*/
    pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES); /*创建kthreadd内核线程,它的作用是管理和调度其它内核线程。*/
    rcu_read_lock();
    kthreadd_task = find_task_by_pid_ns(pid, &init_pid_ns); /*获取kthreadd的线程信息,获取完成说明kthreadd已经创建成功。*/
    rcu_read_unlock();
    complete(&kthreadd_done); /*通过一个complete变量(kthreadd_done)来通知kernel_init线程。*/

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

它完成了一些初始化的操作,和设置一些必要的参数。然后最后一句就是入口了,到此init_task的任务基本上已经完全结束了,它将沦落为一个idle task,事实上在更早前的sched_init()函数中,通过init_idle(current, smp_processor_id())函数的调用就已经把init_task初始化成了一个idle task,init_idle函数的第一个参数current就是&init_task,在init_idle中将会把init_task加入到cpu的运行队列中,这样当运行队列中没有别的就绪进程时,init_task(也就是idle task)将会被调用,它的核心是一个while(1)循环,在循环中它将会调用schedule函数以便在运行队列中有新进程加入时切换到该新进程上。


参考资料:
1.《庖丁解牛Linux操作系统分析》https://gitee.com/mengning997/linuxkernel
2.孟宁、李春杰老师课程《Linux操作系统分析》
3.https://www.cnblogs.com/zhangchao0515/p/5269273.html

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值