Linux内核配置编译及启动过程分析

Linux内核并不能被用户直接使用,发行版才可以。Linux主要的工作是内存管理,进程调度等等,发行版加上了桌面和各种可用的工具,才能被用户使用。

1、内核文件说明

单个文件
(1)Kbuild,Kbuild是kernel build的意思,就是内核编译的意思。这个文件就是linux内核特有的内核编译体系需要用到的文件。
(2)Makefile,这个是linux内核的总makefile,整个内核工程用这个Makefile来管理的。
(3)mk,是九鼎在移植时自己添加的,不是linux内核本身的东西。九鼎添加这个文件的作用是用这个文件来整天管理kernel目录的配置和编译,也就是说这个文件有点类似于我们之前移植uboot时自己创建的那个cp.sh。
文件夹
(1)arch。arch是architecture的缩写,意思是架构。arch目录下是好多个不同架构的CPU的子目录,譬如arm这种cpu的所有文件都在arch/arm目录下,X86的CPU的所有文件都在arch/x86目录下。
(2)block。英文是块的意思,在linux中block表示块设备(以块(多个字节组成的整体,类似于扇区)为单位来整体访问),譬如说SD卡、iNand、Nand、硬盘等都是块设备。你几乎可以认为块设备就是存储设备。block目录下放的是一些linux存储体系中关于块设备管理的代码。
(3)crypto。英文意思是加密。存放加密、压缩、CRC校验等算法相关代码,譬如crc32、md5、sha1等。
(4)Documentation。存放相关说明文档,很多实用文档,包括驱动编写等。
(5)drivers。驱动目录,里面分门别类的列出了linux内核支持的所有硬件设备的驱动源代码。
(6)firmware。固件。什么是固件?固件其实是软件,不过这个软件是固话到IC里面运行的叫固件。就像S5PV210里的iROM代码。
(7)fs。fs就是file system,文件系统,里面列出了linux支持的各种文件系统的实现。
(8)include。头文件目录,公共的(各种CPU架构共用的)头文件都在这里。每种CPU架构特有的一些头文件在arch/arm/include目录及其子目录下。
(9)init。init是初始化的意思,这个目录下的代码就是linux内核启动时初始化内核的代码。
(10)ipc。ipc就是inter process commuication,进程间通信,里面都是linux支持的IPC的代码实现。
(11)kernel。kernel就是内核,就是linux内核,所以这个文件夹下放的就是内核本身需要的一些代码文件。
(12)lib。lib是库的意思,这里面都是一些公用的有用的库函数,注意这里的库函数和C语言的库函数不一样的。在内核编程中是不能用C语言标准库函数,这里的lib目录下的库函数就是用来替代那些标准库函数的。譬如在内核中要把字符串转成数字用atoi,但是内核编程中只能用lib目录下的atoi函数,不能用标准C语言库中的atoi。譬如在内核中要打印信息时不能用printf,而要用printk,这个printk就是我们这个lib目录下的。
(13)mm。mm是memory management,内存管理,linux的内存管理代码都在这里。
(14)net。该目录下是网络相关的代码,譬如TCP/IP协议栈等都在这里。
(15)scripts。脚本,这个目录下全部是脚本文件,这些脚本文件不是linux内核工作时使用的,而是用来辅助对linux内核进行配置编译生产的。我们并不会详细进入分析这个目录下的脚本,而是通过外围来重点学会配置和编译linux内核即可。
(16)security。安全相关的代码。不用去管。
(17)sound。音频处理相关的。
(18)tools。linux中用到的一些有用工具
(19)usr。目录下是早期用户代码(initramfs)相关的,和linux内核的启动有关,暂时不用去管。
(20)virt。内核虚拟机相关的,暂时不用管。
(21)samples。存放提供的一些内核编程范例,如kfifo;后者相关用户态编程范例,如hidraw。

2、内核的配置和编译详解

就像uboot一样,在编译之前要进行配置,才可以去编译
编译之前检查Makefile中ARCH和CROSS_COMPILE,才能去配置,而且Linux源代码必须是在Linux下解压的,如果是从Windows中复制过来的,肯定不会编译成功,Windows不区分大小写,一些文件就会自动合并掉.
1、配置的第一步是make xxx_defconfig,这个xxx_defconfig就是看在在内核根目录下的arch/arm/configs/xxx_defconfig。看打印信息执行完就代表配置成功了。这一步配置会得到一个.config的文件,很重要,里边就是各种配置文件。其实就是Makefile中指定的boards := $(wildcard $(srctree)/arch/$(SRCARCH)/configs/*_defconfig) ,自己添加xxx_defconfig的时候也就是添加一个xxx_config到arch/arm/configs目录下即可
2、第二步:make menuconfig这是第二步的配置,用于精细化的配置,执行成功会得到如下界面

如果没有得到可能的原因是没有安装ncurses库,解决方案: apt-get install libncurses5-dev

menuconfig是利用Kconfig来实现的一个图形化界面。menuconfig中间的选择区中有很多个选择项,每个选择项对应.config文件中的一个配置项,每一个选择项都可以被选择和配置操作,选择区中的每一项都是有子目录的,将光标放在选择项上按Enter键可以进入子目录(子目录可能还会有子目录)。选择区太短放不下所有的一个目录层级的选项,可以用箭头按键的向上箭头和向下箭头来上翻和下翻。键盘按键Y、N、M三个按键的作用分别是将选中模块编入、去除、模块化。双击ESC表示退出,按下?按键可以显示帮助信息,按下/按键可以输入搜索内容来全局搜索信息(类似于vi中的搜索),[ ]不可以模块化,<>的才可以模块化。在menuconfig中选项前面的括号里,*表示编入,空白表示去除,M表示模块化

3、第三步:make
静静的等待个三五分钟即可。
编译完成后得到的内核镜像不在源码树的根目录下,在arch/arm/boot这个目录下。得到的镜像名是zImage
我用Ubuntu18.04编译的时候出现一个错误,Can't use 'defined(@array)' (Maybe you should just omit the defined()?) at kernel/timeconst.pl line 373.,这里就去kernel/timeconst.pl 文件中 373行if (!defined(@val)) 改为if (!@val) 后,编译成功。

3、内核的启动过程代码分析

这里的分析Linux内核的代码不是分析什么内存管理啊,进程调度啊,是按照Linux启动过程来分析的,直到Linux启动起来。
1、链接脚本分析
(1)分析连接脚本的目的就是找到整个程序的entry,kernel的链接脚本并不是直接提供的,而是提供了一个汇编文件vmlinux.lds.S,然后在编译的时候再去编译这个汇编文件得到真正的链接脚本vmlinux.lds。vmlinux.lds.S在arch/arm/kernel/目录下。从vmlinux.lds中ENTRY(stext)可以知道入口符号是stext,搜索这个符号,发现arch/arm/kernel/目录下的head.S和head-nommu.S中都有。head.S是启用了MMU情况下的kernel启动文件,相当于uboot中的start.S。head-nommu.S是未使用mmu情况下的kernel启动文件。
2、head.S文件分析
内核运行的物理地址与虚拟地址
(1)KERNEL_RAM_VADDR(VADDR就是virtual address),这个宏定义了内核运行时的虚拟地址。值为0xC0008000
(2)KERNEL_RAM_PADDR(PADDR就是physical address),这个宏定义内核运行时的物理地址。值为0x30008000
(3)总结:内核运行的物理地址是0x30008000,对应的虚拟地址是0xC0008000。
内核的真正入口
(1)内核的真正入口就是ENTRY(stext)处
(2)前面的__HEAD定义了后面的代码属于段名为.head.text的段

内核运行的硬件条件
(1)内核的起始部分代码是被解压代码调用的。回忆之前讲zImage的时候,uboot启动内核后实际调用运行的是zImage前面的那段未经压缩的解压代码,解压代码运行时先将zImage后段的内核解压开,然后再去调用运行真正的内核入口。
(2)内核启动不是无条件的,而是有一定的先决条件,这个条件由启动内核的bootloader(我们这里就是uboot)来构建保证。
(3)ARM体系中,函数调用时实际是通过寄存器传参的(函数调用时传参有两种设计:一种是寄存器传参,另一种是栈内存传参)。所以uboot中最后theKernel (0, machid, bd->bi_boot_params);执行内核时,运行时实际把0放入r0中,machid放入到了r1中,bd->bi_boot_params放入到了r2中。ARM的这种处理技巧刚好满足了kernel启动的条件和要求。
(4)kernel启动时MMU是关闭的,因此硬件上需要的是物理地址。但是内核是一个整体(zImage)只能被连接到一个地址(不能分散加载),这个连接地址肯定是虚拟地址。因此内核运行时前段head.S中尚未开启MMU之前的这段代码就很难受。所以这段代码必须是位置无关码,而且其中涉及到操作硬件寄存器等时必须使用物理地址。

1、内核启动的汇编阶段
__lookup_processor_type

(1)我们从cp15协处理器的c0寄存器中读取出硬件的CPU ID号(不是机器码),然后调用这个函数来进行合法性检验。如果合法则继续启动,如果不合法则停止启动,转向__error_p启动失败。

(2)该函数检验cpu id的合法性方法是:内核会维护一个本内核支持的CPU ID号码的数组,然后该函数所做的就是将从硬件中读取的cpu id号码和数组中存储的各个id号码依次对比,如果没有一个相等则不合法,如果有一个相等的则合法。

(3)内核启动时设计这个校验,也是为了内核启动的安全性着想。

__lookup_machine_type

(1)该函数的设计理念和思路和上面校验cpu id的函数一样的。不同之处是本函数校验的是机器码。

__vet_atags

(1)该函数的设计理念和思路和上面2个一样,不同之处是用来校验uboot给内核的传参ATAGS格式是否正确。这里说的传参指的是uboot通过tag给内核传的参数(主要是板子的内存分布memtag、uboot的bootargs)

(2)内核认为如果uboot给我的传参格式不正确,那么我就不启动。

(3)uboot给内核传参的部分如果不对,是会导致内核不启动的。譬如uboot的bootargs设置不正确内核可能就会不启动。

__create_page_tables

(1)顾名思义,这个函数用来建立页表。

(2)linux内核本身被连接在虚拟地址处,因此kernel希望尽快建立页表并且启动MMU进入虚拟地址工作状态。但是kernel本身工作起来后页表体系是非常复杂的,建立起来也不是那么容易的。kernel想了一个好办法

(3)kernel建立页表其实分为2步。第一步,kernel先建立了一个段式页表(和uboot中之前建立的页表一样,页表以1MB为单位来区分的),这里的函数就是建立段式页表的。段式页表本身比较好建立(段式页表1MB一个映射,4GB空间需要4096个页表项,每个页表项4字节,因此一共需要16KB内存来做页表),坏处是比较粗不能精细管理内存;第二步,再去建立一个细页表(4kb为单位的细页表),然后启用新的细页表废除第一步建立的段式映射页表。

(4)内核启动的早期建立段式页表,并在内核启动前期使用;内核启动后期就会再次建立细页表并启用。等内核工作起来之后就只有细页表了。

__switch_data

(1)建立了段式页表后进入了__switch_data部分,这东西是个函数指针数组。

(2)分析得知下一步要执行__mmap_switched函数

(3)复制数据段、清除bss段(目的是构建C语言运行环境)

(4)保存起来cpu id号、机器码、tag传参的首地址。

(5)b start_kernel跳转到C语言运行阶段。

开启MMU

总结:汇编阶段其实也没干啥,主要原因是uboot干了大部分活。汇编阶段主要就是校验启动合法性、建立段式映射的页表并开启MMU以方便使用内存、跳入C阶段。

2、内核启动的C语言阶段

开始进入start_kernel(void)函数分析:

杂碎分析

(1)smp。smp就是对称多处理器(其实就是我们说的多核心CPU)

(2)lockdep。锁定依赖,是一个内核调试模块,处理内核自旋锁死锁问题相关的。

(3)cgroup。control group,内核提供的一种来处理进程组的技术。

打印内核版本信息

(1)代码位于:kernel/init/main.c中的572行

(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的内容解析。

#define KERN_EMERG "<0>" /* system is unusable */
#define KERN_ALERT "<1>" /* action must be taken immediately */
#define KERN_CRIT "<2>" /* critical conditions */
#define KERN_ERR "<3>" /* error conditions */
#define KERN_WARNING "<4>" /* warning conditions */
#define KERN_NOTICE "<5>" /* normal but significant condition */
#define KERN_INFO "<6>" /* informational */
#define KERN_DEBUG "<7>" /* debug-level messages */

进入setup_arch分析

setup_arch函数简介

(1)从名字看,这个函数是CPU架构相关的一些创建过程。

(2)实际上这个函数是用来确定我们当前内核的机器(arch、machine)的。我们的linux内核会支持一种CPU的运行,CPU+开发板就确定了一个硬件平台,然后我们当前配置的内核就在这个平台上可以运行。之前说过的机器码就是给这个硬件平台一个固定的编码,以表征这个平台。

(3)当前内核支持的机器码以及硬件平台相关的一些定义都在这个函数中处理。

(4)打印了tags的信息,cmd_line信息

1、Machine查找

(1)setup_processor函数用来查找CPU信息,可以结合串口打印的信息来分析。

(2)setup_machine函数的传参是机器码编号,machine_arch_type符号在include/generated/mach-types.h的32039-32050行定义了。经过分析后确定这个传参值就是2456.

(3)函数的作用是通过传入的机器码编号,找到对应这个机器码的machine_desc描述符,并且返回这个描述符的指针。

(4)其实真正干活的函数是lookup_machine_type,找这个函数发现在head-common.S中,真正干活的函数是__lookup_machine_type

(5)__lookup_machine_type函数的工作原理:内核在建立的时候就把各种CPU架构的信息组织成一个一个的machine_desc结构体实例,然后都给一个段属性.arch.info.init,链接的时候会保证这些描述符会被连接在一起。__lookup_machine_type就去那个那些描述符所在处依次挨个遍历各个描述符,比对看机器码哪个相同。

(6)__lookup_machine_type 函数是由 mdesc = setup_machine(machine_arch_type); 一层一层调用

setup_arch函数进行了基本的cmdline处理

(1)这里说的cmdline就是指的uboot给kernel传参时传递的命令行启动参数,也就是uboot的bootargs。

(2)有几个相关的变量需要注意:

default_command_line:看名字是默认的命令行参数,实际是一个全局变量字符数组,这个字符数组可以用来存东西。

CONFIG_CMDLINE:在.config文件中定义的(可以在make menuconfig中去更改设置),这个表示内核的一个默认的命令行参数。

(3)内核对cmdline的处理思路是:内核中自己维护了一个默认的cmdline(就是.config中配置的这一个),然后uboot还可以通过tag给kernel再传递一个cmdline。如果uboot给内核传cmdline成功则内核会优先使用uboot传递的这一个;如果uboot没有给内核传cmdline或者传参失败,则内核会使用自己默认的这个cmdline。以上说的这个处理思路就是在setup_arch函数中实现的。

uboot的:cmdline:console=ttySAC2,115200 root=/dev/mmcblk0p2 rw init=/linuxrc rootfstype=ext3

内核默认的:cmdline:console=ttySAC2,115200

实验验证内核的cmdline确定

(1)验证思路:首先给内核配置时配置一个基本的cmdline,然后在uboot启动内核时给uboot设置一个bootargs,然后启动内核看打印出来的cmdline和uboot传参时是否一样。

(2)在uboot中去掉bootargs,然后再次启动内核看打印出来的cmdline是否和内核中设置的默认的cmdline一样。

注意:uboot给内核传递的cmdline非常重要,会影响内核的运行,所以要谨慎。有时候内核启动有问题,可以分析下是不是uboot的bootargs设置不对。

注意:这个传参在这里确定出来之后,还没完。后面还会对这个传参进行解析。解析之后cmdline中的每一个设置项都会对内核启动有影响。

setup_command_line

(1)也是在处理和命令行参数cmdline有关的任务。

parse_early_param&parse_args

(1)解析cmdline传参和其他传参

(2)这里的解析意思是把cmdline的细节设置信息给解析出来。譬如cmdline:console=ttySAC2,115200 root=/dev/mmcblk0p2 rw init=/linuxrc rootfstype=ext3,则解析出的内容就是就是一个字符串数组,数组中依次存放了一个设置项目信息。

console=ttySAC2,115200 一个

root=/dev/mmcblk0p2 rw 一个

init=/linuxrc 一个

rootfstype=ext3 一个

(3)这里只是进行了解析,并没有去处理。也就是说只是把长字符串解析成了短字符串,最多和内核里控制这个相应功能的变量挂钩了,但是并没有去执行。执行的代码在各自模块初始化的代码部分。

杂碎

(1)trap_init 设置异常向量表

(2)mm_init 内存管理模块初始化

(3)sched_init 内核调度系统初始化

(4)early_irq_init&init_IRQ 中断初始化

(5)console_init 控制台初始化

总结:start_kernel函数中调用了很多的xx_init函数,全都是内核工作需要的模块的初始化函数。这些初始化之后内核就具有了一个基本的可以工作的条件了。

如果把内核比喻成一个复杂机器,那么start_kernel函数就是把这个机器的众多零部件组装在一起形成这个机器,让他具有可以工作的基本条件。

rest_init

(1)这个函数之前内核的基本组装已经完成。

(2)剩下的一些工作就比较重要了,放在了一个单独的函数中,叫rest_init。

总结:start_kernel函数做的主要工作:打印了一些信息、内核工作需要的模块的初始化被依次调用(譬如内存管理、调度系统、异常处理···)、我们需要重点了解的就是setup_arch中做的2件事情:机器码架构的查找并且执行架构相关的硬件的初始化、uboot给内核的传参cmdline。

进入rest_init函数

(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死循环进程(空闲进程)转而去执行有意义的干活的进程。这样操作系统就转起来了。

什么是内核线程

(1)进程和线程。简单来理解,一个运行的程序就是一个进程。所以进程就是任务、进程就是一个独立的程序。独立的意思就是这个程序和别的程序是分开的,这个程序可以被内核单独调用执行或者暂停。

(2)在linux系统中,线程和进程非常相似,几乎可以看成是一样的。实际上我们当前讲课用到的进程和线程的概念就是一样的。

(3)进程/线程就是一个独立的程序。应用层运行一个程序就构成一个用户进程/线程,那么内核中运行一个函数(函数其实就是一个程序)就构成了一个内核进程/线程。

(4)所以我们kernel_thead函数运行一个函数,其实就是把这个函数变成了一个内核线程去运行起来,然后他可以被内核调度系统去调度。说白了就是去调度器注册了一下,以后人家调度的时候会考虑你。

进程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内核自己本身能正常工作的。

总结1:本节课的重点在于理解linux内核启动后达到的一个稳定状态。注意去对比内核启动后的稳定状态和uboot启动后的稳定状态的区别。

总结2:本节课的第二个重点就是初步理解进程/线程的概念。

总结3:你得明白每个进程有个进程号,进程号从0开始依次分配的。明白进程0是idle进程(idle进程是干嘛的);进程2是ktheadd进程(基本明白干嘛的就行)

总结4:分析到此,发现后续的料都在进程1.所以后面课程会重点从进程1出发,分析之后发生的事情。

init进程详解

init进程完成了从内核态向用户态的转变

(1)一个进程2种状态。init进程刚开始运行的时候是内核态,它属于一个内核线程,然后他自己运行了一个用户态下面的程序后把自己强行转成了用户态。因为init进程自身完成了从内核态到用户态的过度,因此后续的其他进程都可以工作在用户态下面了。

(2)内核态下做了什么?重点就做了一件事情,就是挂载根文件系统并试图找到用户态下的那个init程序。

init进程要把自己转成用户态就必须运行一个用户态的应用程序(这个应用程序名字一般也叫init),要运行这个应用程序就必须得找到这个应用程序,要找到它就必须得挂载根文件系统,因为所有的应用程序都在文件系统中。

kernelinitmain.c static noinline int init_post(void)

1.内核源代码中的所有函数都是内核态下面的,执行任何一个都不能脱离内核态。
2.应用程序必须不属于内核源代码,这样才能保证自己是用户态。
3.也就是说我们这里执行的这个init程序和内核不在一起,他是另外提供的。
4.提供这个init程序的那个人就是根文件系统。

(3)用户态下做了什么?

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

(4)如何从内核态跳跃到用户态?还能回来不?

1.init进程在内核态下面时,通过一个函数kernel_execve来执行一个用户空间编译连接的应用程序就跳跃到用户态了。

注意这个跳跃过程中进程号是没有改变的,所以一直是进程1.

这个跳跃过程是单向的,也就是说一旦执行了init程序转到了用户态下整个操作系统就算真正的运转起来了,以后只能在用户态下工作了,用户态下想要进入内核态只有走API这一条路了。

init进程构建了用户交互界面

(1)init进程是其他用户进程的老祖宗。
linux系统中一个进程的创建是通过其父进程创建出来的。根据这个理论只要有一个父进程就能生出一堆子孙进程了。

(2)init启动了login进程、命令行进程、shell进程

(3)shell进程启动了其他用户进程。

命令行和shell一旦工作了,用户就可以在命令行下通过./xx的方式来执行其他应用程序,每一个应用程序的运行就是一个进程。

总结:本节的主要目的是让大家认识到init进程如何一步步发展成为我们平时看到的那种操作系统的样子。

打开控制台
(1)linux系统中每个进程都有自己的一个文件描述符表,表中存储的是本进程打开的文件。
(2)linux系统中有一个设计理念:一切届是文件。所以设备也是以文件的方式来访问的。我们要访问一个设备,就要去打开这个设备对应的文件描述符。譬如/dev/fb0这个设备文件就代表LCD显示器设备,/dev/buzzer代表蜂鸣器设备,/dev/console代表控制台设备。

(3)这里我们打开了/dev/console文件,并且复制了2次文件描述符,一共得到了3个文件描述符。这三个文件描述符分别是0、1、2.这三个文件描述符就是所谓的:标准输入、标准输出、标准错误。

(4)进程1打开了三个标准输出输出错误文件,因此后续的进程1衍生出来的所有的进程默认都具有这3个三件描述符。

挂载根文件系统
(1)prepare_namespace函数中挂载根文件系统
(2)根文件系统在哪里?

根文件系统的文件系统类型是什么? uboot通过传参来告诉内核这些信息。
uboot传参中的 root=/dev/mmcblk0p2 rw 这一句就是告诉内核根文件系统在哪里
uboot传参中的 rootfstype=ext3 这一句就是告诉内核rootfs的类型。

(3)如果内核挂载根文件系统成功,则会打印出:
VFS: Mounted root (ext3 filesystem) on device 179:2.
如果挂载根文件系统失败,则会打印:
No filesystem could mount root, tried: yaffs2

(4)如果内核启动时挂载rootfs失败,则后面肯定没法执行了,肯定会死。内核中设置了启动失败休息5s自动重启的机制,因此这里会自动重启,所以有时候大家会看到反复重启的情况。

(5)如果挂载rootfs失败,可能的原因有:
最常见的错误就是uboot的bootargs设置不对。
rootfs烧录失败(fastboot烧录不容易出错,以前是手工烧录很容易出错)
rootfs本身制作失败的。(尤其是自己做的rootfs,或者别人给的第一次用)

执行用户态下的进程1程序

(2)我们如果确定init程序是谁?方法是:
1.先从uboot传参cmdline中看有没有指定,如果有指定先执行cmdline中指定的程序。
2.cmdline中的init=/linuxrc这个就是指定rootfs中哪个程序是init程序。
3.这里的指定方式就表示我们rootfs的根目录下面有个名字叫linuxrc的程序,这个程序就是init程序。
如果uboot传参cmdline中没有init=xx或者cmdline中指定的这个xx执行失败,还有备用方案。

第一备用:/sbin/init,
第二备用:/etc/init,
第三备用:/bin/init,
第四备用:/bin/sh。
如果以上都不成功,则认命了,死了。

cmdline常用参数

格式简介
(1)格式就是由很多个项目用空格隔开依次排列,每个项目中都是项目名=项目值
(2)整个cmdline会被内核启动时解析,解析成一个一个的项目名=项目值的字符串。这些字符串又会被再次解析从而影响启动过程。

root=
(1)这个是用来指定根文件系统在哪里的
(2)一般格式是root=/dev/xxx(一般如果是nandflash上则/dev/mtdblock2,如果是inand/sd的话则/dev/mmcblk0p2)
(3)如果是nfs的rootfs,则root=/dev/nfs。

rootfstype=
(1)根文件系统的文件系统类型,一般是jffs2、yaffs2、ext3、ubi

console=
(1)控制台信息声明,譬如console=/dev/ttySAC0,115200表示控制台使用串口0,波特率是115200.
(2)正常情况下,内核启动的时候会根据console=这个项目来初始化硬件,并且重定位console到具体的一个串口上,所以这里的传参会影响后续是否能从串口终端上接收到内核的信息。

mem=
(1)mem=用来告诉内核当前系统的内存有多少

init=
(1)init=用来指定进程1的程序pathname,一般都是init=/linuxrc

常见cmdline介绍
(1)console=ttySAC2,115200 root=/dev/mmcblk0p2 rw init=/linuxrc rootfstype=ext3
第一种这种方式对应rootfs在SD/iNand/Nand/Nor等物理存储器上。这种对应产品正式出货工作时的情况。ro 表示只读
(2)root=/dev/nfs nfsroot=192.168.1.141:/root/s3c2440/build_rootfs/aston_rootfs ip=192.168.1.10:192.168.1.141:192.168.1.1:255.255.255.0::eth0:off init=/linuxrc console=ttySAC0,115200
第二种这种方式对应rootfs在nfs上,这种对应我们实验室开发产品做调试的时候。

4、Linux内核启动地址和开发板的机器码确定

Linux内核启动地址
由kernel配置分析可知,连接地址和他对应的物理地址通过head.S中可以查到,分别是0xC0008000和0x30008000。那么自解压代码配置的解压地址应该是30008000.自解压代码对应的自解压地址在arch/arm/mach-s5pv210/Makefile.boot文件中。在其中修改,加入两行:

#override for SMDKV210
zreladdr-$(CONFIG_MACH_SMDKV210) := 0x30008000
params_phys-$(CONFIG_MACH_SMDKV210) := 0x30000100

还要在\arch\arm\mach-s5pv210\include\mach\memory.h中配置好内存地址才可以

开发板的机器码确定
MACHINE_START宏
这个宏用来定义一个机器码的数据结构的。这个宏的使用其实是用来定义一个结构体类型为machine_desc类型的结构体变量,名为__mach_desc_SMDKV210。这个结构体变量会被定义到一个特定段.arch.info.init,因此这个结构体变量将来会被链接器链接到这个.arch.info.init段中。开发板的机器码就是MACH_TYPE_SMDKV210。

Last modification:November 9th, 2019 at 04:41 pm
如果觉得我的文章对你有用,请随意赞赏

Leave a Comment