内核启动过程分析

举报
dz小伟 发表于 2022/11/11 14:55:35 2022/11/11
【摘要】 ​目录一、内核源码获取查看1.1、Source Insight使用 二、查看链接脚本三、分析head.S3.1、到入口前代码3.2、内核启动的汇编阶段四、main.c内核启动的c语言阶段4.1、内核打印函数printk4.2、启动信息五、rest_init函数5.1、进程0、进程1、进程2​编辑5.2、init进程的2种状态5.3、init进程干了什么六、宏MACHINE_START6.1、...

目录

一、内核源码获取查看

1.1、Source Insight使用

 二、查看链接脚本

三、分析head.S

3.1、到入口前代码

3.2、内核启动的汇编阶段

四、main.c内核启动的c语言阶段

4.1、内核打印函数printk

4.2、启动信息

五、rest_init函数

5.1、进程0、进程1、进程2​编辑

5.2、init进程的2种状态

5.3、init进程干了什么

六、宏MACHINE_START

6.1、结构体 machine_desc

6.2、树莓派内核的MACHINE_START



一、内核源码获取查看

  1. 获取下载
  2. 拿到Ubuntu中解压编译,参考
  3. 打包源码到window下
    tar -czf smp.tar.gz linux-rpi-4.14.y
    cp smp.tar.gz /mnt/hgfs/gongxian-20-04/
  4. 使用Source Insight阅读代码

1.1、Source Insight使用

  1. 创建工程
    编辑
  2. 选择保存路径和工程名称
    编辑
  3. 点击Add Tree
    编辑
  4. 点击,完成最后一步
    编辑

 二、查看链接脚本

  1. kernel的连接脚本并不是直接提供的,而是提供了一个汇编文件vmlinux.lds.S,然后在编译的时候再去编译这个汇编文件得到真正的链接脚本vmlinux.lds
  2. vmlinux.lds.S在arch/arm/kernel/目录下
  3. 从vmlinux.lds中ENTRY(stext)可以知道入口符号是stext
    OUTPUT_ARCH(arm)
    ENTRY(stext)
    jiffies = jiffies_64;
    SECTIONS
    {
     /DISCARD/ : {
      *(.ARM.exidx.exit.text)
      *(.ARM.extab.exit.text)
      *(.ARM.exidx.cpuexit.text)
      *(.ARM.extab.cpuexit.text)
     
     
      *(.exitcall.exit)
      *(.discard)
      *(.discard.*)
     }
     . = 0x80000000 + 0x00008000;
     .head.text : {
      _text = .;
      *(.head.text)
     }
     
  4. 查找发现2个文件中有ENTRY(stext)
    head-nommu.S ,head.S
  5. head.S是启用了MMU情况下的kernel启动文件
    head-nommu.S是未使用mmu情况下的kernel启动文件
    在加上查看编译后的文件发现,有head.o文件,所以可以确定是head.S

三、分析head.S

3.1、到入口前代码

  1. KERNEL_RAM_VADDR,这个宏定义了内核运行时的虚拟地址。
  2. 入口就是ENTRY(stext)处,前面的__HEAD定义了后面的代码属于段名为.head.text的段
  3. 阅读注释
    内核的起始部分代码是被解压代码调用的,内核运行是有条件的
    MMU = 关, D-cache = 关, I-cache = 不在乎, r0 = 0,  r1 = 机器码, r2 = tag地址
    此代码基本上是位置无关的,因此将内核链接到0xc0008000
    有关机器的完整列表,请参见arch/arm/tools/mach-types

3.2、内核启动的汇编阶段

	bl	__hyp_stub_install
	@ ensure svc mode and all interrupts masked
	safe_svcmode_maskall r9

	mrc	p15, 0, r9, c0, c0		@ get processor id
	bl	__lookup_processor_type		@ r5=procinfo r9=cpuid
	movs	r10, r5				@ invalid processor (r5=0)?
 THUMB( it	eq )		@ force fixup-able long branch encoding
	beq	__error_p			@ yes, error 'p'


	ldr	r8, =PLAT_PHYS_OFFSET		@ always constant in this case

	/*
	 * r1 = machine no, r2 = atags or dtb,
	 * r8 = phys_offset, r9 = cpuid, r10 = procinfo
	 */
	bl	__vet_atags

	bl	__fixup_smp

	bl	__fixup_pv_table

	bl	__create_page_tables

	ldr	r13, =__mmap_switched		@ address to jump to after
  1. __hyp_stub_install
    arm虚拟化功能有关
  2. __lookup_processor_type
    从cp15协处理器的c0寄存器中读取出硬件的CPU ID号
    然后调用这个函数来进行合法性检验,如果合法则继续启动,如果不合法则停止启动转向__error_p启动失败
  3. __vet_atags
    校验uboot给内核的传参ATAGS格式是否正确
  4. __create_page_tables
    这个函数用来建立页表
    kernel建立页表其实分为2步:kernel先建立了一个段式页表,再去建立一个细页表
    启动的早期建立段式页表,并在内核启动前期使用;内核启动后期就会建立细页表并启用
  5. __mmap_switched
    复制数据段、清除bss段(目的是构建C语言运行环境)
    保存起来cpu id号、机器码、tag传参的首地址
    b    start_kernel跳转到C语言运行阶段

四、main.c内核启动的c语言阶段

start_kernel函数位于main.c中
位置在:linux-rpi-4.14.y/init/main.c

4.1、内核打印函数printk

  • printk函数的用法和printf几乎一样,不同之处是可以用一个宏来定义消息输出的级别
  • 打印级别,是为了解决打印信息过多,无效信息会淹没有效信息这个问题
  • 给每一个printk添加一个打印级别,级别定义0-7
    分别代表8种输出的重要性级别,0表示最重要,7表示最不重要
     
    #define KERN_SOH	"\001"		/* ASCII Start Of Header */
    #define KERN_SOH_ASCII	'\001'
    
    #define KERN_EMERG	KERN_SOH "0"	/* system is unusable */
    #define KERN_ALERT	KERN_SOH "1"	/* action must be taken immediately */
    #define KERN_CRIT	KERN_SOH "2"	/* critical conditions */
    #define KERN_ERR	KERN_SOH "3"	/* error conditions */
    #define KERN_WARNING	KERN_SOH "4"	/* warning conditions */
    #define KERN_NOTICE	KERN_SOH "5"	/* normal but significant condition */
    #define KERN_INFO	KERN_SOH "6"	/* informational */
    #define KERN_DEBUG	KERN_SOH "7"	/* debug-level messages */
  • linux的控制台有一个消息过滤显示机制,控制台只会显示级别比我的控制台定义的级别高的消息

4.2、启动信息

[    0.000000] Booting Linux on physical CPU 0x0
[    0.000000] Linux version 4.14.114-v7 (xw@ubuntu) (gcc version 4.8.3 20140303 (prerelease) (crosstool-NG linaro-1.13.1+bzr2650 - Linaro GCC 2014.03)) #1 SMP Tue Nov 1 05:49:10 PDT 2022
[    0.000000] CPU: ARMv7 Processor [410fd034] revision 4 (ARMv7), cr=10c5383d
。。。。
[    0.000000] Kernel command line: 8250.nr_uarts=1 bcm2708_fb.fbwidth=656 bcm2708_fb.fbheight=416 bcm2708_fb.fbswap=1 vc_mem.mem_base=0x3ec00000 vc_mem.mem_size=0x40000000  dwc_otg.lpm_enable=0 console=tty1 console=ttyAMA0,115200 root=/dev/mmcblk0p2 rootfstype=ext4 elevator=deadline fsck.repair=yes rootwait
  1. 启动信息前面的[]数字是启动所用时间
  2. 第一条启动信息是smp_setup_processor_id();函数打印的
    smp就是对称多处理器
    它读取当前物理CPU,映射到逻辑CPU0,设置当前task id为0,打印启动信息
  3. 第二条是pr_notice("%s", linux_banner);打印的
    pr_notice可以理解为是printk函数的封装形式
  4. setup_arch   位置:arch/arm/kernel/setup.c
    setup_processor函数用来查找CPU信息
  5. parse_early_param  解析cmdline传参和其他传参
  6. trap_init:设置异常向量表
    mm_init:内存管理模块初始化
    console_init:控制台初始化
    等一系列初始化。。。

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

五、rest_init函数

  1. rest_init中调用kernel_thread函数启动了2个内核线程,分别是:kernel_init和kthreadd
  2. 调用schedule_preempt_disabled->schedule函数开启了内核的调度系统,从此linux系统开始转起来了
  3. 最终调用cpu_startup_entry->do_idle函数结束了整个内核的启动。也就是说linux内核最终结束了一个函数cpu_idle
  4. linux内核最终的状态是:有事干的时候去执行有意义的工作,没活干的时候就去死循环
    调度系统

5.1、进程0、进程1、进程2
编辑

  1. ubuntu下ps -aux可以看到当前系统运行的所有进程
    进程0不是一个用户进程,而属于内核进程,所以不显示0进程
  2. 进程0:进程0就是idle进程,叫空闲进程,也就是死循环
    进程1:kernel_init函数就是进程1,这个进程被称为init进程
    进程2:kthreadd函数就是进程2,这个进程是linux内核的守护进程。这个进程是用来保证linux内核自己本身能正常工作的

5.2、init进程的2种状态

  1. init进程2种状态,内核态向用户态的转变
  2. init进程在内核态下面时,通过一个函数do_execve来执行一个用户空间编译连接的应用程序就跳跃到用户态了。注意这个跳跃过程中进程号是没有改变的
  3. init进程刚开始运行的时候是内核态,它属于一个内核线程,然后他自己运行了一个用户态下面的程序后把自己强行转成了用户态
  4. init进程在内核态:挂载根文件系统并试图找到用户态下的那个init程序
    init程序和内核不在一起,在根文件下
  5. init进程在用户态:所有的用户进程都直接或者间接派生自init进程
  6. init启动了login进程、命令行进程、shell进程

5.3、init进程干了什么

  1. 打开了/dev/console控制台文件
    复制了2次文件描述符,一共得到了3个文件描述符
    这三个文件描述符分别是012,这三个文件描述符就是:标准输入、标准输出、标准错误
    后续的进程1衍生出来的所有的进程默认都具有这3个三件描述符
  2. 内核态下的进程1,挂载根文件系统
    kernel_init->kernel_init_freeable->prepare_namespace函数中挂载根文件系统
    root=/dev/mmcblk0p2  这一句就是告诉内核根文件系统在哪里
    rootfstype=ext4   告诉内核rootfs文件系统的类型
    挂载成功   [    2.395288] VFS: Mounted root (ext4 filesystem) readonly on device 179:2.
  3. 用户态下的进程1
    kernel_init->run_init_process执行用户态下的init程序

六、宏MACHINE_START

#define MACHINE_START(_type,_name)			\
static const struct machine_desc __mach_desc_##_type	\
 __used							\
 __attribute__((__section__(".arch.info.init"))) = {	\
	.nr		= MACH_TYPE_##_type,		\
	.name		= _name,

#define MACHINE_END				\
};
  1. 定义了一个结构体类型为machine_desc的结构体变量
  2. 这个结构体变量会被定义到一个特定段.arch.info.init
  3. 会被链接器链接到这个.arch.info.init段中
     .init.arch.info : {
      __arch_info_begin = .;   //开始
      *(.arch.info.init)
      __arch_info_end = .;       //结尾
     }

6.1、结构体 machine_desc

struct machine_desc {
	unsigned int		nr;		/* architecture number	*/
	const char		*name;		/* architecture name	*/
	unsigned long		atag_offset;	/* tagged list (relative) */
	const char *const 	*dt_compat;	/* array of device tree
						 * 'compatible' strings	*/

	unsigned int		nr_irqs;	/* number of IRQs */

#ifdef CONFIG_ZONE_DMA
	phys_addr_t		dma_zone_size;	/* size of DMA-able area */
#endif

	unsigned int		video_start;	/* start of video RAM	*/
	unsigned int		video_end;	/* end of video RAM	*/

	unsigned char		reserve_lp0 :1;	/* never has lp0	*/
	unsigned char		reserve_lp1 :1;	/* never has lp1	*/
	unsigned char		reserve_lp2 :1;	/* never has lp2	*/
	enum reboot_mode	reboot_mode;	/* default restart mode	*/
	unsigned		l2c_aux_val;	/* L2 cache aux value	*/
	unsigned		l2c_aux_mask;	/* L2 cache aux mask	*/
	void			(*l2c_write_sec)(unsigned long, unsigned);
	const struct smp_operations	*smp;	/* SMP operations	*/
	bool			(*smp_init)(void);
	void			(*fixup)(struct tag *, char **);
	void			(*dt_fixup)(void);
	long long		(*pv_fixup)(void);
	void			(*reserve)(void);/* reserve mem blocks	*/
	void			(*map_io)(void);/* IO mapping function	*/
	void			(*init_early)(void);
	void			(*init_irq)(void);
	void			(*init_time)(void);
	void			(*init_machine)(void);
	void			(*init_late)(void);
#ifdef CONFIG_MULTI_IRQ_HANDLER
	void			(*handle_irq)(struct pt_regs *);
#endif
	void			(*restart)(enum reboot_mode, const char *);
};
  1. nr:机器码
  2. name:开发板名字
  3. init_machine:硬件驱动的加载和初始化函数执行

6.2、树莓派内核的MACHINE_START

  1. 经过查找,发现BCM_2835机器码为4828
    用的文件是board_bcm2835.c
    grep "MACHINE_START(*BCM2835*" * -nr
    tools/mach-types:553:bcm2835			MACH_BCM2835		BCM2835			4828
    arch/arm/mach-bcm/board_bcm2835.c:126:DT_MACHINE_START(BCM2835, "BCM2835")
  2. 使用函数bcm2835_init,硬件驱动的加载和初始化函数执行
    DT_MACHINE_START(BCM2835, "BCM2835")
    	.map_io = bcm2835_map_io,
    	.init_machine = bcm2835_init,
    	.init_early = bcm2835_init_early,
    	.dt_compat = bcm2835_compat,
    	.smp = smp_ops(bcm2836_smp_ops),
    MACHINE_END
    
  3. 通过宏可以看出,树莓派没有用到机器码

#define DT_MACHINE_START(_name, _namestr)		\
static const struct machine_desc __mach_desc_##_name	\
 __used							\
 __attribute__((__section__(".arch.info.init"))) = {	\
	.nr		= ~0,				\
	.name		= _namestr,
【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。