......
上面提及过,硬碟中MBR是由GRUB写入的boot.img,因而这儿的linux/arch/x86/boot/head.S中的bootsector对于硬碟启动是无用的。GRUB将vmlinuz的setup.bin部份读到显存地址0x90000处,之后跳转到0x90200开始执行,正好跳过了上面512字节的bootsector,从_start开始。
16位的main函数
我们一般用C编译器编译的代码,是32位保护模式下的或则是64位长模式的,却极少编译成16位实模式下的,虽然setup.bin大部份代码都是16位实模式下的。从上面的代码里,我们就能看见在linux/arch/x86/boot/head.S中调用了main函数,该函数在linux/arch/x86/boot/main.c文件中,代码如下
里面那些函数都在linux/arch/x86/boot/目录对应的文件中,都是调用BIOS中断完成的,具体细节,你可以自行查看。我这儿列举的代码只是帮助你理清流程,我们继续瞧瞧go_to_protected_mode()函数,在linux/arch/x86/boot/pm.c中,代码如下。
protected_mode_jump是个汇编函数,在linux/arch/x86/boot/pmjump.S文件中即跳转到boot_params.hdr.code32_start中的地址。这个地址在linux/arch/x86/boot/head.S文件中设为0x100000,如下所示
须要注意的是,GRUB会把vmlinuz中的vmlinux.bin部份,置于1MB开始的显存空间中。通过这一跳转,即将步入vmlinux.bin中。
startup_32函数
startup_32中须要重新加载段描述符,然后估算vmlinux.bin文件的编译生成的地址和实际加载地址的偏斜,之后重新设置内核栈linux服务器代维,测量CPU是否支持长模式,接着再度估算vmlinux.bin加载地址的偏斜,来确定对其中vmlinux.bin.gz解压缩的地址。假如CPU支持长模式的话,就要设置64位的全局描述表,开启CPU的PAE化学地址扩展特点。再设置最初的MMU页表,最后开启分页并步入长模式,跳转到startup_64,代码如下。
startup_64函数如今,我们总算开启了CPU长模式,从startup_64开始真正步入了64位的时代,可喜可贺。startup_64函数同样也是在linux/arch/x86/boot/compressed/head64.S文件中定义的。startup_64函数中,初始化长模式下数据段寄存器,确定最终解压缩地址,之后拷贝压缩vmlinux.bin到该地址,跳转到decompress_kernel地址处,开始解压vmlinux.bin.gz,
上述代码中最后到了extract_kernel函数,它就是解压内核的函数,下边我们就来研究它。
extract_kernel函数
从startup_32函数到startup_64函数,其间经过了保护模式、长模式,最终抵达了extract_kernel函数,extract_kernel函数依据piggy.o中的信息从vmlinux.bin.gz中解压出vmlinux。依照上面的知识点,我们晓得vmlinux正是编译出Linux内核elf格式的文件,只不过它被除去了符号信息。所以,extract_kernel函数不仅仅是解压,还须要解析elf格式。extract_kernel函数是在linux/arch/x86/boot/compressed/misc.c文件中定义的
正如前面代码所示,extract_kernel函数调用__decompress函数,对vmlinux.bin.gz使用特定的解压算法进行解压。解压算法是编译内核的配置选项决定的。并且,__decompress函数解压下来的是vmlinux文件是elf格式的,所以还要调用parse_elf函数进一步解析elf格式,把vmlinux中的指令段、数据段、BSS段,按照elf中信息和要求装入特定的显存空间,返回指令段的入口地址。
请你注意,在Lrelocated函数的最后一条指令:jmp*rax,其中的rax中就是保存的extract_kernel函数返回的入口点,就是从这儿开始步入了Linux内核
Linux内核的startup_64
这儿我提醒你留心,此时的startup_64函数并不是之前的startup_64函数,也不参与后面的链接工作。这个startup_64函数定义在linux/arch/x86/kernel/head_64.S文件中,它是内核的入口函数,如下所示。
上述代码中省略了和流程无关的代码,对于SMP系统加电以后,总线仲裁机制会选出多个CPU中的一个CPU,称为BSP,也叫第一个CPU。它负责让BSPCPU先启动,其它CPU则等待BSPCPU的唤起。
这儿我来分情况给你聊聊。对于第一个启动的CPU,会跳转secondary_startup_64函数中1标号处,对于其它被唤起的CPU则会直接执行secondary_startup_64函数。接出来,我给你快速过一遍secondary_startup_64函数,旁边的代码我省略了这个函数对更多CPU特点(设置GDT、IDT,处理了MMU页表等)的检测,由于这种工作我们已经很熟悉了,代码如下所示。
在secondary_startup_64函数一切打算就绪以后,最后都会调用x86_64_start_kernel函数,看它的名子似乎是内核的开始函数,但真的是这样吗,我们一起瞧瞧才晓得
Linux内核的第一个C函数
若不是经历了上面的剖析讲解。要是我问你Linux内核的第一个C函数是哪些,你可能无从说起,即使一通百度以后,一直未能确定.
然而,只要我们跟随代码的执行流程,都会发觉在secondary_startup_64函数的最后,调用的x86_64_start_kernel函数是用C语言写的,这么它一定就是Linux内核的第一个C函数。它在linux/arch/x86/kernel/head64.c文件中被定义,这个文件名你甚至都能猜下来,如下所示。
x86_64_start_kernel函数中又一次处理了页表,处理页表就是处理Linux内核虚拟地址空间,Linux虚拟地址空间是一步步构建的。之后,x86_64_start_kernel函数复制了引导信息,即structboot_params结构体。最后调用了x86_64_start_reservations函数,其中处理了平台固件相关的东西,就是调用了大名鼎鼎的start_kernel函数。
有名的start_kernel函数
start_kernel函数之所以有名linux vm,这是由于在互联网上,在各大Linux名著之中,就会大量宣传它Linux内核中的地位和作用linux操作系统论文,正如其名子抒发的含义,这是内核的开始。并且问题来了。我们一路走来,发觉start_kernel函数之前有大量的代码执行,那这种代码算不算内核的开始呢?其实也可以说那就是内核的开始,也可以说是前期工作。
虽然,start_kernel函数中调用了大量Linux内核功能的初始化函数,它定义在/linux/init/main.c文件中。
start_kernel函数我若果不做精简,会有200多行,全部都是初始化函数,我只留下几个主要的初始化函数,这种函数的实现细节我们无需关心。可以看见,Linux内核所有功能的初始化函数都是在start_kernel函数中调用的,这也是它这么出名,这么重要的诱因。一旦start_kernel函数执行完成,Linux内核就具备了向应用程序提供一系列功能服务的能力。这儿对我们而言,我们只关注一个arch_call_rest_init函数。下边我们就来研究它。如下所示。
rest_init函数的重要功能就是构建了两个Linux内核线程,我们瞧瞧精简后的rest_init函数:
Linux内核线程可以执行一个内核函数,只不过这个函数有独立的线程上下文,可以被Linux的进程调度器调度,对于kernel_init线程来说,执行的就是kernel_init函数
Linux的第一个用户进程
当我们可以构建第一个用户进程的时侯,就代表Linux内核的初始流程早已基本完成。经历了“长途跋涉”,我们也总算走到了这儿。Linux内核的第一个用户态进程是在kernel_init线程构建的,而kernel_init线程执行的就是kernel_init函数。那kernel_init函数究竟做了哪些呢?
结合上述代码,可以发觉ramdisk_execute_command和execute_command都是内核启动时传递的参数,它们可以在GRUB启动选项中设置。比方说,一般引导内核时向commandline传递的参数都是init=xxx,而对于initrd则是传递rdinit=xxx。
然而,一般我们不会传递参数,所以这个函数会执行到上述代码的15行,依次尝试以/sbin/init、/etc/init、/bin/init、/bin/sh那些文件为可执行文件构建进程,而且只要其中之一成功就行了。
try_to_run_init_process和run_init_process函数的核心都是调用sys_fork函数构建进程的,这儿我们不用关注它的实现细节。到这儿,Linux内核早已构建了第一个进程,Linux内核的初始化流程也到此为止。
重点回顾
又到了课程尾声,Linux初始化流程的学习我们就告一段落了,我来给你做个总结。
明天我们讲得内容有点多,我们从_start开始到startup32、startup64函数,到extract_kernel函数解压出真正的Linux内核文件vmlinux开始,之后从Linux内核的入口函数startup_64到Linux内核第一个C函数,最后接着从Linux内核start_kernel函数的完善,说到了第一个用户进程。
一上去回顾一下这节课的重点:
1.GRUB加载vmlinuz文件以后,会把控制权交给vmlinuz文件的setup.bin的部份中_start,它会设置好栈,清空bss,设置好setup_header结构linux vm,调用16位main切换到保护模式,最后跳转到1MB处的vmlinux.bin文件中。
2.从vmlinux.bin文件中startup32、startup64函数开始构建新的全局段描述符表和MMU页表,切换到长模式下解压vmlinux.bin.gz。释放出vmlinux文件以后,由解析elf格式的函数进行解析,释放vmlinux中的代码段和数据段到指定的显存。之后调用其中的startup_64函数,在这个函数的最后调用Linux内核的第一个C函数。
3.Linux内核第一个C函数重新设置MMU页表,此后便调用了最有名的start_kernel函数,start_kernel函数中调用了大多数Linux内核功能性初始化函数,在最后调用rest_init函数构建了两个内核线程,在其中的kernel_init线程构建了第一个用户态进程。
本文原创地址://q13zd.cn/ljxypqdzmygd.html编辑:刘遄,审核员:暂无