内核启动过程总结
之前配置编译过内核源代码,在交叉编译源代码后产生了三个文件(还有其他文件)分别是vmlinuz、vmlinux、vmlinux32,其中vmlinuz是可引导的、压缩了的内核,将该内核拷贝到系统文件/boot目录下,再配置下/boot/boot.cfg文件,将启动时选择内核的信息和加载内核的地方写入就可以实现内核的移植。其实移植过程和正常内核启动过程的原理是一样的。
系统加电启动后,MIPS处理器默认的程序入口时0xBFC00000,此地址在无缓存的KSEG1的地址区域内,对应的物理地址是0x1FC00000,即CPU从0x1FC00000开始取第一条指令,内核是系统引导程序把内核加载到内存中的,如果内核是经过压缩的,那么首先执行/arch/mips/boot/compressed的head.S文件去建立堆栈并解压内核映像文件,然后去执行/arch/mips/kernel下的head.S,如果是没有压缩的内核则直接去执行该head.S。linux内核启动的第一阶段就是从kernel文件夹下的head.S开始的,kernel_entry()函数就是内核启动的入口函数,这个函数是与体系结构相关的汇编语言编写的,它首先初始化内核堆栈段,来为创建系统的第一个进程0进程作准备,接着用一段循环将内核映像的未初始化数据段bss段清零,最后跳转到/init/main.c中的start_kernel()初始化硬件平台相关的代码。
kernel_entry() - arch/mips/kernel/head.S
TLB初始化,Cache初始化
清除BSS段
准备参数 argc/argp/envp
设置栈
jal start_kernel (init/main.c)
怎么为第一个进程0进程作准备?
第一个进程涉及到init_thread_union,这个结构体在include/linux/sched.h得到定义
extern union thread_union init_thread_union;
union thread_union{
struct thread_info thread_info;
unsigned long stack[THREAD_SIZE/sizeof(long)];
};
THREAD_SIZE是一个宏定义#define THREAD_SIZE (2*PAGE_SIZE)
#define PAGE_SIZE (_AC(1,UL)<<PAGE_SHIFT) PAGE_SHIFT=13 算出PAGE_SIZE=2^12=4096;则THREAD_SIZE=8192
内核把进程存放在任务队列的双向循环链表中,链表中的每一项都是类型为task_struct、称为进程描述符的结构,进程描述符中包含一个具体进程的所有信息,linux通过slab分配器分配task_struct结构,每个任务都有一个thread_info结构,它在内核栈的尾部分配,结构中的task域中存放的是指向该任务实际task_struct的指针,thread_info结构在文件<asm/thread_info.h>中定义。
struct thread_info {
struct task_struct *task; /* main task structure */
struct exec_domain *exec_domain; /* execution domain */
unsigned long flags; /* low level flags */
unsigned long tp_value; /* thread pointer */
__u32 cpu; /* current CPU */
int preempt_count; /* 0 => preemptable, <0 => BUG */
mm_segment_t addr_limit; /* thread address space:
0-0xBFFFFFFF for user-thead
0-0xFFFFFFFF for kernel-thread */
struct restart_block restart_block;
struct pt_regs *regs;
};
/*
* Initial thread structure.
*
* We need to make sure that this is 8192-byte aligned due to the
* way process stacks are handled. This is done by making sure
* the linker maps this in the .text segment right after head.S,
* and making head.S ensure the proper alignment.
*
* The things we do for performance..
*/
union thread_union init_thread_union __init_task_data
__attribute__((__aligned__(THREAD_SIZE))) =
{ INIT_THREAD_INFO(init_task) };
__init_task_data是一个宏,在"include/linux/init_task.h"中这样定义的
#define __init_task_data __attribute__((__section__(“.data..init_task”)))
这是一条赋值语句,对init_thread_union赋初值,并且把数据放在指定的数据段.data..init_task中。具体如何赋值,看看init_task这个全局变量
/*
* Initial task structure.
*
* All other task structs will be allocated on slabs in fork.c
*/
struct task_struct init_task = INIT_TASK(init_task);
0号进程的task_struct出现了,就是init_task。在对init_thread_union赋初值的时候,同时也通过调用INIT_TASK宏对init_task赋初值:
#define INIT_TASK(tsk) \
{ \
.state = 0, \
.stack = &init_thread_info, \
.usage = ATOMIC_INIT(2), \
.flags = PF_KTHREAD, \
.lock_depth = -1, \
.prio = MAX_PRIO-20, \
.static_prio = MAX_PRIO-20, \
.normal_prio = MAX_PRIO-20, \
.policy = SCHED_NORMAL, \
.cpus_allowed = CPU_MASK_ALL, \
.mm = NULL, \
.active_mm = &init_mm, \
.se = { \
.group_node = LIST_HEAD_INIT(tsk.se.group_node), \
}, \
.rt = { \
.run_list = LIST_HEAD_INIT(tsk.rt.run_list), \
.time_slice = HZ, \
.nr_cpus_allowed = NR_CPUS, \
}, \
.tasks = LIST_HEAD_INIT(tsk.tasks), \
.pushable_tasks = PLIST_NODE_INIT(tsk.pushable_tasks, MAX_PRIO), \
.ptraced = LIST_HEAD_INIT(tsk.ptraced), \
.ptrace_entry = LIST_HEAD_INIT(tsk.ptrace_entry), \
.real_parent = &tsk, \
.parent = &tsk, \
.children = LIST_HEAD_INIT(tsk.children), \
.sibling = LIST_HEAD_INIT(tsk.sibling), \
.group_leader = &tsk, \
.real_cred = &init_cred, \
.cred = &init_cred, \
.cred_guard_mutex = \
__MUTEX_INITIALIZER(tsk.cred_guard_mutex), \
.comm = "swapper", \
.thread = INIT_THREAD, \
.fs = &init_fs, \
.files = &init_files, \
.signal = &init_signals, \
.sighand = &init_sighand, \
.nsproxy = &init_nsproxy, \
.pending = { \
.list = LIST_HEAD_INIT(tsk.pending.list), \
.signal = {{0}}}, \
.blocked = {{0}}, \
.alloc_lock = __SPIN_LOCK_UNLOCKED(tsk.alloc_lock), \
.journal_info = NULL, \
.cpu_timers = INIT_CPU_TIMERS(tsk.cpu_timers), \
.fs_excl = ATOMIC_INIT(0), \
.pi_lock = __RAW_SPIN_LOCK_UNLOCKED(tsk.pi_lock), \
.timer_slack_ns = 50000, /* 50 usec default slack */ \
.pids = { \
[PIDTYPE_PID] = INIT_PID_LINK(PIDTYPE_PID), \
[PIDTYPE_PGID] = INIT_PID_LINK(PIDTYPE_PGID), \
[PIDTYPE_SID] = INIT_PID_LINK(PIDTYPE_SID), \
}, \
.thread_group = LIST_HEAD_INIT(tsk.thread_group), \
.dirties = INIT_PROP_LOCAL_SINGLE(dirties), \
INIT_IDS \
INIT_PERF_EVENTS(tsk) \
INIT_TRACE_IRQFLAGS \
INIT_LOCKDEP \
INIT_FTRACE_GRAPH \
INIT_TRACE_RECURSION \
INIT_TASK_RCU_PREEMPT(tsk) \
}
可以看到0号进程的stack就是刚才init_thread_info的地址;parent是他自己;thread是INIT_THREAD。附上进程描述符的task_struct结构图
0号进程task_struct有了,下面就该来讲讲INIT_THREAD_INFO宏了,在arch/mips/include/asm/thread_info.h:
/*
* macros/functions for gaining access to the thread information structure
*/
#define INIT_THREAD_INFO(tsk) \
{ \
.task = &tsk, \
.exec_domain = &default_exec_domain, \
.flags = _TIF_FIXADE, \
.cpu = 0, \
.preempt_count = INIT_PREEMPT_COUNT, \
.addr_limit = KERNEL_DS, \
.restart_block = { \
.fn = do_no_restart_syscall, \
}, \
}
执行完这个宏以后,init_thread_union就被初始化成以上内容了,至此,0号进程的task_struct和thread_info就初始化完毕了。
怎么将未初始化数据段bss段清零呢?看源代码
PTR_LA t0, __bss_start # clear .bss
LONG_S zero, (t0)
PTR_LA t1, __bss_stop - LONGSIZE
1:
PTR_ADDIU t0, LONGSIZE
LONG_S zero, (t0)
bne t0, t1, 1b
最后跳转到 /init/main.c中的start_kernel()初始化硬件平台相关的代码。
*********************************************************************
附上/kernel/head.S源代码及部分注释:
.macro setup_c0_status set clr
#ifdef CONFIG_MIPS_MT_SMTC
...........
#else
mfc0 t0, CP0_STATUS #取CP0 status寄存器的值到临时寄存器to中
or t0, ST0_CU0|\set|0x1f|\clr
xor t0, 0x1f|\clr
mtc0 t0, CP0_STATUS #将临时寄存器to中的值赋给CP0状态寄存器
.set noreorder #表示禁止为了填充加载指令和分支指令的延迟槽而对代 #码重新排序
sll zero,3 # ehb (exception hazard barrier)
.macro setup_c0_status_pri
-----------------------------------------------------
#ifdef CONFIG_64BIT
setup_c0_status ST0_KX 0
#else
setup_c0_status 0 0
#endif
.endm
------------------------------------------------------------------------------------------
NESTED(kernel_entry, 16, sp) # kernel entry point声明函数 kernel_entry,函数的堆栈为 16 byte,返回地址保存在 $sp 寄存器中。
kernel_entry_setup #cpu specific setup,某些MIPS CPU需要额外的设置一些
控制寄存器,和具体的平台相关,一般为空宏;某些多
核MIPS,启动时所有的core的入口一起指向kernel_entry,
然后在该宏里分叉,boot core 继续往下,其它的则不停
的判断循环,直到boot core 唤醒之
setup_c0_status_pri #设置cp0_status 寄存器
PTR_LA t0, 0f
jr t0
PTR_LA t0, __bss_start # clear .bss
LONG_S zero, (t0) #变量 __bss_start 和 __bss_stop 在连接文件arch/mips/kernel/vmlinux.lds 中定义。
PTR_LA t1, __bss_stop - LONGSIZE
1:
PTR_ADDIU t0, LONGSIZE
LONG_S zero, (t0)
bne t0, t1, 1b
LONG_S a0, fw_arg0 # firmware arguments
LONG_S a1, fw_arg1 #bootloader会将要传给内核的参数写在
LONG_S a2, fw_arg2 a0~a3里,此处为将参数保存在fw_arg0-fw_arg3中
LONG_S a3, fw_arg3
MTC0 zero, CP0_CONTEXT # clear context register
PTR_LA $28, init_thread_union #初始化gp,指向一个 union,THREAD_SIZE大小,最低处是一个thread_info结构体
PTR_LI sp, _THREAD_SIZE - 32 - PT_SIZE #_THERAD_SIZE=16384
PT_SIZE=304(sizeof(struct pt_regs))
PTR_ADDU sp, $28
back_to_back_c0_hazard
set_saved_sp sp, t0, t1 #把这个CPU核的堆栈地址 $sp保存到 kernelsp[NR_CPUS]数组。
----------------------------------------------------------------
如果定义了 CONFIG_SMP 宏,即多 CPU 核。
.macro set_saved_sp stackp temp temp2
#ifdef CONFIG_MIPS_MT_SMTC
mfc0 \temp, CP0_TCBIND
#else
MFC0 \temp, CP0_CONTEXT
#endif
LONG_SRL \temp, PTEBASE_SHIFT
LONG_S \stackp, kernelsp(\temp)
.endm
如果没有定义 CONFIG_SMP 宏,单 CPU 核。
.macro set_saved_sp stackp temp temp2
LONG_S \stackp, kernelsp
.endm
变量 kernelsp 的定义,在 arch/mips/kernel/setup.c 文件中。
unsigned long kernelsp[NR_CPUS];
-------------------------------------------------------------------------------------------------------
PTR_SUBU sp, 4 * SZREG # init stack pointer SZREG=8
j start_kernel #最后跳转到 /init/main.c中的start_kernel()初始化硬件平台相关的代码。
END(kernel_entry)
*********************************************************************
#define asmlinkage __attribute__((regparm(0)))
_attribute__是关键字,是gcc的C语言扩展,regparm(0)表示不从寄存器传递参数。如果是__attribute__((regparm(3))),
那么调用函数的时候参数不是通过栈传递,而是直接放到寄存器里,被调用函数直接从寄存器取参数
函数定义前加宏asmlinkage ,表示这些函数通过堆栈而不是通过寄存器传递参数。gcc编译器在汇编过程中调用c语言函数时
传递参数有两种方法:一种是通过堆栈,另一种是通过寄存器。缺省时采用寄存器,假如你要在你的汇编过程中调用c语言函数,
并且想通过堆栈传递参数,你定义的c函数时要在函数前加上宏asmlinkage
*********************************************************************
看看init/main.c源代码
asmlinkage void __init start_kernel(void){
char *command_line;
extern const struct kernel_param __start__param[],__stop__param[];
//来自外部的/include/linux/moduleparam.h中的kernel_param结构体 这两个变量为地址指针,指向内核启动参数处理相关结构体段在内存中的位置(虚拟地址)。
smp_setup_processor_id();//当只有一个CPU的时候这个函数什么都不做,但是如果有多个CPU的时候它就返回启动的时候的那个CPU的id号
unwind_init();
在MIPS体系结构中,这个函数是个空函数(可能调用setup_arch,配置核的相关函数)
lockdep_init();初始化核依赖关系哈希表
lockdep是一个内核调试模块,用来检查内核互斥机制(尤其是自旋锁)潜在的死锁问题。由于自旋锁以查询方式等待,不释放处理器,比一般互斥机制更容易死锁,故引入lockdep检查以下几种可能的死锁情况:
1.同一个进程递归地加锁同一把锁;
2.一把锁既在中断(或中断下半部)使能的情况下执行过加锁操作, 又在中断(或中断下半部)里执行过加锁操作。这样该锁有可能在锁定时由于中断发生又试图在同一处理器上加锁;
3.加锁后导致依赖图产生成闭环,这是典型的死锁现象。
启动Lock Dependency Validator(内核依赖的关系表),本质上就是建立两个散列表calsshash_table和chainhash_table,并初始化全局变量lockdep_initialized,标志已初始化完成
debug_objects_early_init();//在启动早期初始化hash buckets 和链接静态的 pool objects对象到 poll 列表. 在这个调用完成后 object tracker 已经开始完全运作了.
boot_init_stack_canary();canary值的是用于防止栈溢出攻击的堆栈的保护字 。参考资料: GCC 中的编译器堆栈保护技术
cgroup_init_early();cgroup: 它的全称为control group.即一组进程的行为控制.该函数主要是做数据结构和其中链表的初始化
local_irp_disable(); //关闭系统总中断(底层调用汇编指令)
early_boot_irqs_off();//设置系统中断的关闭标志(bool全局变量)
early_init_irp_lock_class();每个中断都有一个中断描述符(struct irq_desc)来进行描述,这个函数的作用就是设置所有中断描述符的锁
tick_init();如果没有定义 CONFIG_GENERIC_CLOCKEVENTS宏定义,则这个函数为空函数,如果定义了这个宏,这执行初始化 tick控制功能,注册clockevents的框架。
boot_cpu_init();对于 CPU 核的系统来说,设置第一个 CPU 核为活跃 CPU 核。对于单 CPU 核系统来说,设置 CPU 核为活跃 CPU 核
page_address_init();当定义了CONFIG_HIGHMEM 宏,并且没有定义 WANT_PAGE_VIRTUAL 宏时,非空函数。其他情况为空函数。
printk("KERN_NOTICE "%s",linux_banner);
setup_arch(&command_line);内核构架相关初始化函数,可以说是非常重要的一个初始化步骤。其中包含了处理器相关参数的初始化、内核启动参数(tagged list)的获取和前期处理、内存子系统的早期的初始化(bootmem分配器)。
mm_init_owner(&init_mm,&init_task);初始化代表内核本身内存使用的管理结构体init_mm。ps:每一个任务都有一个mm_struct结构以管理内存空间,init_mm是内核的mm_struct,其中:设置成员变量* mmap指向自己,意味着内核只有一个内存管理结构;
setup_command_line(command_line);
保存未改变的 comand_line 到字符数组 static_command_line[] 中。保存 boot_command_line到字符数组 saved_command_line[]中。
setup_nr_cpu_ids();
setup_per_cpu_areas();如果没有定义 CONFIG_SMP宏,则这个函数为空函数。如果定义了CONFIG_SMP宏,则这个 setup_per_cpu_areas()函数给每个CPU分配内存,并拷贝.data.percpu段的数据。
smp_prepare_boot_cpu();
build_all_zonelists(NULL);建立各个节点的管理区的 zonelist,便于分配内存的 fallback使用。
这个链表的作用: 这个链表是为了在一个分配不能够满足时可以考察下一个管理区来设置了。在考察结束时,分配将从 ZONE_HIGHMEM回退到 ZONE_NORMAL,在分配时从 ZONE_NORMAL退回到 ZONE_DMA就不会回退了。
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);
。。。。。。。。此处省略很多行
rest_init();
}
*********************************************************************
其中有个函数是与自己体系结构相关的函数setup_arch()
在kernel/setup.c中在里面看到
unsigned long kernelsp[NR_CPUS];
unsigned long fw_arg0, fw_arg1, fw_arg2, fw_arg3;
void __init setup_arch(char **cmdline_p)
{
cpu_probe();
调用函数cpu_probe(),该函数通过MIPS CPU的PRID寄存器来确定CPU类型,
从而确定使用的指令集和其他一些CPU参数,如TLB等
prom_init();
prom_init() 函数是和硬件相关的,做一些低层的初始化,接受引导装载程序传给内核的参数,确定 mips_machgroup,mips_machtype 这两个变量,这两个变量分别对应着相应的芯片组合开发板;
打印cpu_probe() 函数检测到的CPU 的Processor ID。
如果有浮点处理器,也打印浮点处理器的Processor ID。
cpu_report();
应用程序通过终端接口设备使用特定的接口规程与终端进行交互,与操作系统内核本身交互的终端称为控制台,
它可以是内核本身的内部显示终端,也可以是通过串口连接的外部哑终端。
由于大多数情况下控制台都是内核显示终端,因此内核显示终端也常常直接称为控制台。
内核终端对用户来说具有若干个虚拟终端子设备,它们共享同一物理终端,
但同一时刻只能有一个虚拟终端操作硬件屏幕。
宏 CONFIG_VT 的意思是否支持虚拟终端。
当配置了宏 CONFIG_VGA_CONSOLE 时为内核本身的内部显示终端。
当配置了宏 CONFIG_DUMMY_CONSOLE 时为通过串口连接的外部哑终端。
用变量 conswitchp 来进行指定。
#if defined(CONFIG_VT)
#if defined(CONFIG_VGA_CONSOLE)
conswitchp = &vga_con;
#elif defined(CONFIG_DUMMY_CONSOLE)
conswitchp = &dummy_con;
#endif
#endif
对内存进行初始化。
arch_mem_init(cmdline_p);
这个函数遍历每一个内存空间范围(物理地址),在资源管理器中进行资源申请,并对内核代码和数据段进行资源申请。
resource_init();
#ifdef CONFIG_SMP
plat_smp_setup();
#endif
}
在内核初始化函数start_kernel执行到最后,就是调用rest_init函数,这个函数的主要使命就是创建并启动内核线程init
static noinline void __init_refok rest_init(void)
__releases(kernel_lock)
{
int pid;
rcu_scheduler_starting();#内核RCU锁机制调度启动
kernel_thread(kernel_init, NULL, CLONE_FS | CLONE_SIGHAND);
#创建kernel_init内核线程,内核的1号进程!!!!kernel_init 是要执行的函数的指针, NULL 表示传递给该函数的参数为空, CLONE_FS | CLONE_SIGHAND 为 do_fork 产生线程时的标志,表示进程间的 fs 信息共享,信号处理和块信号共享
numa_default_policy();
pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);
1 创建kthreadd内核线程,它的作用是管理和调度其它内核线程。
2 它循环运行一个叫做kthreadd的函数,该函数的作用是运行kthread_create_list全局链表中维护的内核线程。
3 调用kthread_create创建一个kthread,它会被加入到kthread_create_list 链表中;
4 被执行过的kthread会从kthread_create_list链表中删除;
5 且kthreadd会不断调用scheduler函数让出CPU。此线程不可关闭。
rcu_read_lock();
kthreadd_task = find_task_by_pid_ns(pid, &init_pid_ns);
rcu_read_unlock();
complete(&kthreadd_done);
获取kthreadd的线程信息,获取完成说明kthreadd已经创建成功。并通过一个complete变量(kthreadd_done)来通知kernel_init线程。
/*
* The boot idle thread must execute schedule()
* at least once to get things moving:
*/
init_idle_bootup_task(current);
preempt_enable_no_resched();使能抢占,但不重新调度
schedule();执行调度 切换进程
preempt_disable(); 进程调度完成回到这里禁用抢占
/* Call into cpu_idle with preempt disabled */
cpu_idle();
此时内核本体进入了idle状态,用循环消耗空闲的CPU时间片,该函数从不返回。在有其他进程需要工作的时候,该函数就会被抢占!
}
0号进程和1号进程我们现在都知道是怎么产生的,但是有个疑问就是start_kernel()中的rest_init()执行完毕后,内核就怎样启动了呢。
kernel_init()函数中do_basic_setup()函数主要是初始化设备驱动,完成其他驱动程序(直接编译进内核的模块)的初始化。内核中大部分的启动数据输出(都是个设备的驱动模块输出)都是这里产生的。
接下来是打开根文件系统中的/dev/console,此处不可失败
/* Open the /dev/console on the rootfs, this should never fail */
if (sys_open((const char __user *) "/dev/console", O_RDWR, 0) < 0)
printk(KERN_WARNING "Warning: unable to open an initial console.\n");
这是kernel_init打开的第一个文件,它也成为了标准输入,这里需要打开/dev/console,如果没有这个节点,系统就出错,这个错误是经常碰到的,可能的原因是:
1、制作文件系统的时候忘记创建/dev/console节点
2、文件系统挂载问题,挂载上的文件系统不是什么都没有就是挂错了节点
(void)sys_dup(0);
(void)sys_dup(0);
复制两次标准输入(0)的文件描述符(它是上面打开的/dev/console,也就是系统控制台:
一个作为标准输出(1) 一个作为标准出错(2)
现在是标准输入标准输出标准出错都是/dev/console了
· /*
· * 检查是否有早期用户空间的init程序。如果有,让其执行
· *
· */
·
· 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, 我们已经完成了启动初始化, and
· * 且我们本质上已经在运行。释放初始化用的内存(initmem)段
· * 并开始用户空间的程序..
· */
·
· init_post();
· return 0;
· }
在内核线程的最后执行了init_post函数,在这个函数中真正启动了用户空间进程init,这是一个非__init函数,强制让它成为非内联函数,以防gcc让它内敛到init()中并成为init.text段的一部分,从此函数名可知,这个函数是运行在用户控件的init程序之前
static noinline int init_post(void) __releases(kernel_lock){ }
current->signal->flags |= SIGNAL_UNKILLABLE;
设置当前进程(init)为不可杀进程(忽略致命的信号)
if (ramdisk_execute_command) {
run_init_process(ramdisk_execute_command);
printk(KERN_WARNING "Failed to execute %s\n",
ramdisk_execute_command);
}
如果ramdisk_execute_command有指定的init程序,就执行它
/*
* 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) {
run_init_process(execute_command);
printk(KERN_WARNING "Failed to execute %s. Attempting "
"defaults...\n", execute_command);
}
run_init_process("/sbin/init");
run_init_process("/etc/init");
run_init_process("/bin/init");
run_init_process("/bin/sh");
在检查完ramdisk_execute_command和execute_command为空的情况下,顺序执行以下初始化程序:如果都没有找到就打印错误信息。在做系统移植的时候经常碰到这样的错误信息,出现这个信息很可能是因为:
1.启动参数配置有问题,指定了init程序,但是没有找到,且默认的那四个程序文件也不在文件系统中。
2.文件系统挂载有问题,文件不存在
3.init程序没有执行权限
至此内核的初始化结束,正式进入用户空间的初始化过程。
这篇总结中大致讲了下内核启动的过程、源代码的功能作用以及某些函数存在的意义,从中可以初步了解到内核被加载后做的工作以及怎么让内核启动从而从内核空间转向用户空间,主要知识都是查资料总结的,里面还有很多函数的功能以及代码的实现都没有总结,我会通过以后的学习慢慢积累完善。