itop4412内核的启动过程分析

《itop4412内核的启动过程分析》


--------------------------------------------------------
参考《朱老师物联网大讲堂》uboot和系统移植-第16部分
www.zhulaoshi.org
很大部分参考朱老师的课件,最过分的是还有直接复制粘贴的,一方面实在是不会表达,另一方面是真的不会,引用一下,见怪莫怪。可能会有错误,希望指正。
在此也为朱有鹏老师打个广告,有兴趣可以去了解一下《朱老师物联网大讲堂》
--------------------------------------------------------


一、head.S文件分析,内核启动的汇编阶段
1.重要宏定义
(1)#define KERNEL_RAM_VADDR (PAGE_OFFSET + TEXT_OFFSET)
  定义内核运行时的虚拟地址,PAGE_OFFSET=CONFIG_PAGE_OFFSET=0xC0000000,TEXT_OFFSET=0x00008000,KERNEL_RAM_VADDR=0xc0008000
  注:对于CONFIG_xxx这样的宏,一般可以尝试在.config文件中找
  
2.内核真正入口(head.S)
(1)内核的真正入口为 ENTRY(stext)
(2)前面的__HEAD定义了后面的代码属于段名为.head.text的段


3.__lookup_processor_type
(1)读cp15协处理器的c0获得CPU ID号,然后调用__lookup_processor_type来进行合法性检验。如果合法则继续启动,如果不合法则停止启动,转向__error_p启动失败。
(2)该函数检验cpu id的合法性方法是:内核会维护一个本内核支持的CPU ID号码的数组,然后该函数所做的就是将从硬件中读取的cpu id号码和数组中存储的各个id号码依次对比,
   如果没有一个相等则不合法,如果有一个相等的则合法。
   
4.__vet_atags
(1)__vet_atags用来校验uboot给内核的传参ATAGS格式是否正确。这里说的传参指的是uboot通过tag给内核传的参数(主要是板子的内存分布memtag、uboot的bootargs)
(2)uboot给内核传参的部分如果不对,是会导致内核不启动的。譬如uboot的bootargs设置不正确内核可能就会不启动。


5.__create_page_tables
(1)此个函数用来建立页表。
(2)linux内核本身被连接在虚拟地址处,因此kernel希望尽快建立页表并且启动MMU进入虚拟地址工作状态。但是kernel本身工作起来后页表体系是非常复杂的,
建立起来也不是那么容易的。kernel想了一个好办法
(3)kernel建立页表其实分为2步。第一步,kernel先建立了一个段式页表(和uboot中之前建立的页表一样,页表以1MB为单位来区分的),这里的函数就是建立段式页表的。
段式页表本身比较好建立(段式页表1MB一个映射,4GB空间需要4096个页表项,每个页表项4字节,因此一共需要16KB内存来做页表),坏处是比较粗不能精细管理内存;
第二步,再去建立一个细页表(4kb为单位的细页表),然后启用新的细页表废除第一步建立的段式映射页表。
(4)内核启动的早期建立段式页表,并在内核启动前期使用;内核启动后期就会再次建立细页表并启用。等内核工作起来之后就只有细页表了。


6.__mmap_switched
(1)建立了段式页表后进入了由__mmap_switched跳到__switch_data部分,这东西是个函数指针数组。
(2)然后是复制数据段、清除bss段(目的是构建C语言运行环境)
(3)保存起来cpu id号、机器码、tag传参的首地址。
(4)b start_kernel跳转到C语言运行阶段。


二、内核启动的C语言阶段
1.零散内容,不用深究
(1)smp_setup_processor_id(),smp就是对称多处理器(其实就是我们说的多核心CPU)
(2)lockdep_init(),锁定依赖,是一个内核调试模块,处理内核自旋锁死锁问题相关的。
(3)cgroup_init_early(),control group,内核提供的一种来处理进程组的技术。


2.串口打印信息
(1)printk(KERN_NOTICE "%s", linux_banner);
(2)printk函数是内核中用来从console打印信息的,类似于应用层编程中的printf。内核编程时不能使用标准库函数,因此不能使用printf,
其实printk就是内核自己实现的一个printf。
(3)printk函数的用法和printf几乎一样,不同之处在于可以在参数最前面用一个宏来定义消息输出的级别。为什么要有这种级别?主要原因是linux内核太大了,
   代码量太多,里面的printk打印信息太多了。如果所有的printk都能打印出来而不加任何限制,则最终内核启动后得到海量的输出信息。
(4)为了解决打印信息过多,无效信息会淹没有效信息这个问题,linux内核的解决方案是给每一个printk添加一个打印级别。级别定义0-7(注意编程的时候要用相应的宏定义,不要直接用数字)
   分别代表8种输出的重要性级别,0表示最重要,7表示最不重要。我们在printk的时候自己根据自己的消息的重要性去设置打印级别。
(5)linux的控制台监测消息的地方也有一个消息过滤显示机制,控制台实际只会显示级别比我的控制台定义的级别高的消息。譬如说控制台的消息显示级别设置为4,
   那么只有printk中消息级别为0-3(也可能是0-4)的才可以显示看见,其余的被过滤掉了。
(6)linux_banner的内容解析。


3.setup_arch(&command_line)函数
(1)从名字看,这个函数是CPU架构相关的一些创建过程。
(2)实际上这个函数是用来确定我们当前内核的机器(arch、machine)的。我们的linux内核会支持一种CPU的运行,CPU+开发板就确定了一个硬件平台,
   然后我们当前配置的内核就在这个平台上可以运行。之前说过的机器码就是给这个硬件平台一个固定的编码,以表征这个平台。
(3)当前内核支持的机器码以及硬件平台相关的一些定义都在这个函数中处理。


4.setup_arch函数内的几个重要调用
(1)setup_processor(),此函数用来查找CPU信息,可以结合串口打印的信息来分析。
(2)setup_machine_tags(machine_arch_type),函数的传参是机器码编号,machine_arch_type符号在include/generated/mach-types.h中定义了。
   经过分析后确定这个传参值就是3698.你会发现这机器码怎么和uboot(2838)传过来的不一样?这样内核还能启动?我猜的原因是machine_arch_type 直接被赋值为3698,而,没有用uboot传过来的机器码(__machine_arch_type),有兴趣可以修改一下uboot的机器码,看内核是否还能启动。
(3)函数的作用是通过传入的机器码编号,找到对应这个机器码的machine_desc描述符,并且返回这个描述符的指针。
(4)其实真正干活的函数是#define for_each_machine_desc(p) for (p = __arch_info_begin; p < __arch_info_end; p++),这个宏
(5)工作原理:内核在建立的时候就把各种CPU架构的信息组织成一个一个的machine_desc结构体实例,然后都给一个段属性.arch.info.init,链接的时候会保证这些描述符会被连接在一起。
for (p = __arch_info_begin; p < __arch_info_end; p++)就去那个那些描述符所在处依次挨个遍历各个描述符,比对看机器码哪个相同。
(6)可以看出,这里的machid查找,作用只是找到我们内核维护的machine_desc结构体实例中我们开发板的那个machine_desc,这东西在mach-itop4412.c的结尾处定义。
(7)对比另外的内核源代码,发现在__lookup_processor_type之后有 bl __lookup_machine_type,顾名思义这个函数校验的是机器码,值得注意的是它是与r1相比较的,而r1存的正好是uboot传过来的机器码,但我们的内核源码是没有这个的跳转,也验证了我们的内核根本没用uboot传过来的机器码,为什么不用呢,大概是uboot中的维护机器码和内核维护的机器码对不上,若调用__lookup_machine_type对应的汇编代码,保存r1里的机器码与machine_desc对不上导致内核启动失败。


5.setup_machine_tags对cmdline的基本处理
(1)char *from = default_command_line,default_command_line是存放command里呢的数组,默认值由CONFIG_CMDLINE在.config定义
(2)内核对commandline处理:
一是默认值。
二是uboot的传参,若传参正确,default_command_line 的值由传参决定,然后复制给boot_command_line。但是看itop的内核源码,在iTop4412_Kernel_3.0\arch\arm\kernel下的setup.c中parse_tag_cmdline函数
调用了strlcat(default_command_line, " ", COMMAND_LINE_SIZE);
     strlcat(default_command_line, tag->u.cmdline.cmdline,
 COMMAND_LINE_SIZE);  作用是把传进来的cmdline加在后面
若使用itop提供的uboot是经过修改 ,实现了环境变量的保存的,可以删掉这些,只留下strlcpy(default_command_line, tag->u.cmdline.cmdline,
COMMAND_LINE_SIZE);和return 0;就可以保证自己传过来的参数的正确性。

parse_tag_cmdline被__tagtable(ATAG_CMDLINE, parse_tag_cmdline)初始化在__tagtable列表,在parse_tags(tags)被执行。


6.parse_early_param&parse_args
(1)解析cmdline传参和其他传参
(2)这里的解析意思是把cmdline的细节设置信息给解析出来。譬如cmdline: root=/dev/mmcblk0p2 rootfstype=ext4 init=/linuxrc console=ttySAC2,115200,
   则解析出的内容就是就是一个字符串数组,数组中依次存放了一个设置项目信息。
root=/dev/mmcblk0p2  一个
rootfstype=ext4  一个
init=/linuxrc 一个
console=ttySAC2,115200 一个
(3)这里只是进行了解析,并没有去处理。也就是说只是把长字符串解析成了短字符串,最多和内核里控制这个相应功能的变量挂钩了,但是并没有去执行。执行的代码在各自模块初始化的代码部分。
(4)我们的uboot传参:安卓的内核传参只有console=ttySAC2,115200,对于挂载根文件系统不是很懂,猜测是我们使用了ramdisk,挂载部分由其指定。


7.零散内容,不用深究
(1)trap_init 设置异常向量表
(2)mm_init 内存管理模块初始化
(3)sched_init 内核调度系统初始化
(4)early_irq_init&init_IRQ 中断初始化
(5)console_init 控制台初始化
总结:start_kernel函数中调用了很多的xx_init函数,全都是内核工作需要的模块的初始化函数。这些初始化之后内核就具有了一个基本的可以工作的条件了。
如果把内核比喻成一个复杂机器,那么start_kernel函数就是把这个机器的众多零部件组装在一起形成这个机器,让他具有可以工作的基本条件。


8.rest_init
(1)这个函数之前内核的基本组装已经完成。
(2)剩下的一些工作就比较重要了,放在了一个单独的函数中,叫rest_init。




9.操作系统去哪了
(1)rest_init中调用kernel_thread函数启动了2个内核线程,分别是:kernel_init和kthreadd
(2)调用schedule函数开启了内核的调度系统,从此linux系统开始转起来了。
(3)rest_init最终调用cpu_idle函数结束了整个内核的启动。也就是说linux内核最终结束了一个函数cpu_idle。这个函数里面肯定是死循环。
(4)简单来说,linux内核最终的状态是:有事干的时候去执行有意义的工作(执行各个进程任务),实在没活干的时候就去死循环(实际上死循环也可以看成是一个任务)。
(5)之前已经启动了内核调度系统,调度系统会负责考评系统中所有的进程,这些进程里面只有有哪个需要被运行,调度系统就会终止cpu_idle死循环进程
  (空闲进程)转而去执行有意义的干活的进程。这样操作系统就转起来了。


10.进程简介
(1)进程和线程。简单来理解,一个运行的程序就是一个进程。所以进程就是任务、进程就是一个独立的程序。独立的意思就是这个程序和别的程序是分开的,
   这个程序可以被内核单独调用执行或者暂停。
(2)在linux系统中,线程和进程非常相似,几乎可以看成是一样的。实际上我们当前讲课用到的进程和线程的概念就是一样的。
(3)进程/线程就是一个独立的程序。应用层运行一个程序就构成一个用户进程/线程,那么内核中运行一个函数(函数其实就是一个程序)就构成了一个内核进程/线程。
(4)所以我们kernel_thead函数运行一个函数,其实就是把这个函数变成了一个内核线程去运行起来,然后他可以被内核调度系统去调度。说白了就是去调度器注册了一下,
   以后人家调度的时候会考虑你。


11.进程0、进程1、进程2
(1)截至目前为止,我们一共涉及到3个内核进程/线程。
(2)操作系统是用一个数字来表示/记录一个进程/线程的,这个数字就被称为这个进程的进程号。这个号码是从0开始分配的。因此这里涉及到的三个进程分别是linux系统的进程0、进程1、进程2.
(3)在linux命令行下,使用ps命令可以查看当前linux系统中运行的进程情况。
(4)我们在ubuntu下ps -aux可以看到当前系统运行的所有进程,可以看出进程号是从1开始的。为什么不从0开始,因为进程0不是一个用户进程,而属于内核进程。
(5)三个进程
进程0:进程0其实就是刚才讲过的idle进程,叫空闲进程,也就是死循环。
进程1:kernel_init函数就是进程1,这个进程被称为init进程。
进程2:kthreadd函数就是进程2,这个进程是linux内核的守护进程。这个进程是用来保证linux内核自己本身能正常工作的。


12.init进程详解1 --- 初始化
(1)由kernel_thread(kernel_init, NULL, CLONE_FS | CLONE_SIGHAND)函数创建
(2)do_basic_setup(),此函数用于各种初始化,其中do_initcalls()函数是需要注意一下的。
(3)do_initcalls进行了大部分初始化,实现原理和驱动有关,还没有学不是很懂,大概是各项驱动的初始化函数在其所在的.c文件被定义连接到特定的位置,
在iTop4412_Kernel_3.0\include\linux\init.h中有相关的宏定义
(4)以rootfs_initcall(populate_rootfs)为例,在\iTop4412_Kernel_3.0\init\initramfs.c最后面定义,在init.h中有宏定义
   #define rootfs_initcall(fn) __define_initcall("rootfs",fn,rootfs)
   替换掉后:#define __define_initcall("rootfs",populate_rootfs,rootfs) \
static initcall_t __initcall_populate_rootfsrootfs __used \
__attribute__((__section__(".initcallrootfs.init"))) = populate_rootfs
(5)验证这个对与错:输出的打印信息有[    1.280383] Trying to unpack rootfs image as initramfs... ,证明populate_rootfs()函数被调用。(安卓内核)


13.init进程详解2 --- 大局把握
13.1 init进程完成了从内核态向用户态的转变
(1)一个进程2种状态。init进程刚开始运行的时候是内核态,它属于一个内核线程,然后他自己运行了一个用户态下面的程序后把自己强行转成了用户态。
  因为init进程自身完成了从内核态到用户态的过度,因此后续的其他进程都可以工作在用户态下面了。
(2)内核态下做了什么?重点就做了一件事情,就是挂载根文件系统并试图找到用户态下的那个init程序。init进程要把自己转成用户态就必须运行一个用户态的应用程序
 (这个应用程序名字一般也叫init),要运行这个应用程序就必须得找到这个应用程序,要找到它就必须得挂载根文件系统,因为所有的应用程序都在文件系统中。
  内核源代码中的所有函数都是内核态下面的,执行任何一个都不能脱离内核态。应用程序必须不属于内核源代码,这样才能保证自己是用户态。
  也就是说我们这里执行的这个init程序和内核不在一起,他是另外提供的。提供这个init程序的那个人就是根文件系统。


(3)用户态下做了什么?init进程大部分有意义的工作都是在用户态下进行的。init进程对我们操作系统的意义在于:其他所有的用户进程都直接或者间接派生自init进程。


(4)如何从内核态跳跃到用户态?还能回来不?
  init进程在内核态下面时,通过一个函数kernel_execve来执行一个用户空间编译连接的应用程序就跳跃到用户态了。注意这个跳跃过程中进程号是没有改变的,
  所以一直是进程1.这个跳跃过程是单向的,也就是说一旦执行了init程序转到了用户态下整个操作系统就算真正的运转起来了,以后只能在用户态下工作了,
  用户态下想要进入内核态只有走API这一条路了。


13.2 init进程构建了用户交互界面
(1)init进程是其他用户进程的老祖宗。linux系统中一个进程的创建是通过其父进程创建出来的。根据这个理论只要有一个父进程就能生出一堆子孙进程了。
(2)init启动了login进程、命令行进程、shell进程
(3)shell进程启动了其他用户进程。命令行和shell一旦工作了,用户就可以在命令行下通过./xx的方式来执行其他应用程序,每一个应用程序的运行就是一个进程。


14. init进程详解3 --- 小心求证
14.1 打开控制台
(1)linux系统中每个进程都有自己的一个文件描述符表,表中存储的是本进程打开的文件。
(2)linux系统中有一个设计理念:一切届是文件。所以设备也是以文件的方式来访问的。我们要访问一个设备,就要去打开这个设备对应的文件描述符。
  譬如/dev/fb0这个设备文件就代表LCD显示器设备,/dev/buzzer代表蜂鸣器设备,/dev/console代表控制台设备。
(3)do_basic_setup函数结束后,我们打开了/dev/console文件,并调用sys_dup()复制了2次文件描述符,一共得到了3个文件描述符。这三个文件描述符分别是0、1、2.这三个文件描述符就是所谓的:
  标准输入、标准输出、标准错误。
(4)进程1打开了三个标准输出输出错误文件,因此后续的进程1衍生出来的所有的进程默认都具有这3个三件描述符。

14.2 挂载根文件系统?(安卓内核)
(1)说到根文件系统的挂载,就要提到ramdisk了,由于时间问题没怎么上网查有关的资料,懵懵懂懂的,自己去搜
(2)在uboot阶段,我们把ramdisk加载到了内存40df0000处,而ramdisk_execute_command = "/init"就是在指定我们要运行的用户态的程序
(3)通过打印信息,猜测根文件系统在ext4_init_fs()函数中被挂载,以module_init(ext4_init_fs)形式定义,在某个时间点被调用
(3)init_post()中run_init_process(ramdisk_execute_command)被调用,从此一去不复返

14.3 挂载根文件系统?(QT内核)

(1)测试挂载文件系统与ramdisk的关系,在ramdisk分区随意烧写一个镜像(不要太大)
fastboot flash ramdisk 4412/QT/u-boot-iTOP-4412.bin
测试成功,QT系统挂载不需要ramdisk,但不要烧一个正确的ramdisk进去,否则会出错
(2)根据打印信息,这里走的是另一条挂载路线,不需要ramdisk,挂载的函数在prepare_namespace里面,具体自己可以分析。

15.进程详解部分根据自己理解然后修改得到,不知道不知道对不对,再次感谢朱老师,毕竟大部分内容来自他的课件


2018年5月23
第二次修改2018年5月27
   DGY(改)










 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值