linux操作系统启动流程

据大家所知linux操作系统有uboot kernel rootfs那么他们分别是干啥的呢,下面详细说来。

1.什么是uboot

1.1 uboot简介

Boot 是一个主要用于嵌入式系统的引导加载程序,可以支持多种不同的计算机系统结构,包括PPC、ARM、AVR32、MIPS、x86、68k、Nios与MicroBlaze。这也是一套在GNU通用公共许可证之下发布的自由软件。boot种类众多,uboot是其中之一,本篇着重说明uboot。

1.2 嵌入式引导程序

介绍嵌入式引导程序,首先需要介绍单片机和单板机。在单片机上和单板机嵌入式引导程序有着较大的差别。同样带操作系统的嵌入式引导程序又有其特别要求之处。

1.2.1 单片机

单片机(Single-Chip Microcomputer)单片微电脑,单片机具有电脑所需的一切。比如USB外设 集成LCD控制器 内存 存储器。
单片机样品
![单片机](https://img-blog.csdnimg.cn/cbabf0bf9384444f970219c4997e5e39.webp#pic_center)
相比PC而言,单片机麻雀虽小五脏俱全。往往在实际使用中,为了简单 成本低廉而在只设计了极少部分外设的硬件电路。

1.2.1.1 单片机引导程序原理

重所周之,芯片厂商在每出一款芯片时需将厂商引导代码固化到芯片ROM里面去。由于单片机是针对低端市场而设计的,则引导程序应不需要负杂的外且设成本低廉。并且单片机主打一片一体化集成简单使用的功能,复杂的外设会增加使用技术成本。
引导流程如下:
厂商ROM代码
读外部引脚模式
正常启动用户代码模式
初始化内部flash储器
PC指针指向内部flash存储器地址
用户自己的程序
同时单片机还需要有用户代码固化流程:
厂商ROM代码
读外部引脚模式
下载程序模式
初始化内部flash储器
将通过串口将程序固化到内部flash存储器

1.2.2 单板机

单板机是单板微型计机系统,该系统是由多个集成芯片组成,并且这些集成芯片的功能往往比较单一且性能强大。相较于单片机,单板机性能强劲且设计复杂。

随着芯片设计水平的不断提升,往往某一领域芯片突飞猛进。为了这单一芯片的改进,而重新设计整个芯片架构而不太现实。因此单版机往往是多个功能单一且性能强大的芯片组合而成,并且接口均尽量设计标准化,以方便随意更换其在某一领域单元功能芯片。比如SDRAM SDRAM2 DDR2 DDR3 DDR4这些内存芯片的升级较为频繁。

单板机最小系统如下:
id
1.2.2.1 单板机引导程序原理
随着CPU性能的逐渐提升,微型计算机的瓶颈逐渐出现在内存上。相对与内存,flash存储器的读写速度更是望尘莫及。因此单板机的程序不能简单照抄单片机的模式,之后程序运行在内存上被大家所熟知并刻画在认知领域(单片机程序是运行在flash上)


引导程序流程:

厂商ROM代码运行
初始化储存器外设
将用户代码从外部存储器拷贝到内部RAM
PC指针转至内部RAM地址
内部ram是不需要初始化的随机存储器。无论是单片机还是单板机中的CPU集成芯片,内部都有一定大小的RAM。

因为单板机有着功能多样的外设,因此很少考虑下载程序模式 正常运行模式的切换。通过外部手段将程序提前下载到外部flash存储器,成了大多数设计着的做法。比如zynq15eg 的sd卡启动模式(程序提前存储好了在sd卡)。

1.2.3 带操作系统的嵌入式引导程序原理

此处操作系统是大型操作系统比如linux之内的。关于嵌入式常用的rtos操作系统过于简单,其引导加载如同单片机引导加载原理,此处不再做说明。
操作系统主要功能是管理硬件和文件资料,而管理硬件需要做到硬件抽象成统一接口,让用户方便的使用该硬件处理文件资料。针对嵌入式的rtos之类的小众化操作系统严格来说并不具备硬件抽象成统一接口的能力,因此严格讲并不属于书面的操作系统定义。本文章主要说明人们所熟知的大型操作系统linux的嵌入式引导程序原理。

1.2.3.1 嵌入式linux操作系统引导原理

单片机由于性能低下不能满足嵌入式linux操作系统的要求,因此嵌入式linux操作系统的引导说明是从单板机类型上面讲解。
接上文单板机的引导原理,程序受限与CPU内部RAW大小。为了突破这个限制,用户程序还需要做一些额外操作。
厂商ROM代码运行
初始化储存器外设
将用户代码从外部存储器拷贝到内部RAM32KB
PC指针转至内部RAM地址
运行UBOOT
初始化DDR
初始化外部存储设备
将全部的UBOOT代码拷贝至内存
PC指针代码重定向到DDR内存UBOOT地址
UBOOT代码二次初始化
初始化高级别的硬件驱动
加载设备树到内存
加载kernel到内存
传递设备树地址和kernel启动命令到kernel
跳转至kernel代码处
因为CPU内不RAM大小是有限的,目前大部分UBOOT已经到达了MB级别的代码量了,所以需要将代码存储在DDR。又因为UBOOT有时候需要作一些其他的附属功能,比如电源管理和开机提示界面显示,所以UBOOT也需要像kernel驱动那样完全的支持硬件全部驱动。
随着代码的发展,UBOOT代码风格逐渐朝kernel靠拢,UBOOT也同时出现了数据和驱动分离的设计模式,也就是UBOOT的设备树出现。

2. linux kernel启动

2.1 linux kernel基本认识

上面说到uboot做了一些底层硬件驱动的初始化,没有这些初始化kernel是无法运行起来的。kernel的目的是实现硬件管理和硬件抽象化,因此kernel需要尽可能的兼容所有硬件所有平台。那么kernel的设计之初就是奔着硬件通用化去的,这一点是和UBOOT有着很大的区别。
类比:
linux kernel在X86平台的驱动经过重新编译可以直接用于arm平台,反之亦然。在arm平台设备树资源为dt_ids,在x86平台设备树资源为acpi_ids。
static struct spi_driver spidev_spi_driver = {
     .driver = {
         .name =     "spidev",
         .of_match_table = of_match_ptr(spidev_dt_ids),
         .acpi_match_table = ACPI_PTR(spidev_acpi_ids),
     },
     .probe =    spidev_probe,
     .remove =   spidev_remove,
在x86平台的硬件显然和arm平台的硬件不一样。从上面spi驱动可以看出,一个标准的驱动是需要将硬件处理逻辑写成驱动,而寄存器写成数据,在不同的平台通过识别不同的设备树来兼容所有的硬件,从而表现出驱动完全兼容所有平台的特点。
可以看出kernel是一个硬件和软件沟通的桥梁,它的设计之初重点并不是去如何的驱动硬件,这是很多驱动工程师的误区。kernel并不会向uboot那样去考虑很多底层硬件的初始化,kernel分为四大模块之一的驱动也只是关注用户需要直接用到的硬件驱动。因为用户不会直接去操作DDR,所以DDR驱动并不存在与kernel。
此时应有以下的认识:
1. kernel的着重点不是驱动硬件
2. kernel里面的不包含DDR驱动
3. kernel的运行基础是DDR已经初始化完毕
4. kernel的任务重点是提供各种以硬件为基础的软件服务给到应用层
到此我门可以把kernel理解为一个sdk,向应用层提供各种服务,应用层代码是运行在这个sdk之上的,这个sdk就是操作系统。
kerne 有4个模块
1. 驱动
2. 内存管理
3. 进程管理
4. 文件系统
每一个模块都代表这一个领域,本文章之对驱动模块进行详细说明。

2.2 kernel 启动流程

boot符号
设置CPU运行等级
清除相关寄存器用于运行C代码
C代码start_kernel处
解析bootargs参数
解析设备树为platform_device节点
初始化内存管理
初始化控制台打印
加载驱动driver节点
驱动节点probe初始化硬件
初始化进程管理
挂载分区
根分区寻找init进程
运行init进程
kernel启动关键代码
asmlinkage __visible void __init __no_sanitize_address start_kernel(void)
{
	char *command_line;
	char *after_dashes;

	...
	/*
	 * Interrupts are still disabled. Do necessary setups, then
	 * enable them.
	 */
	boot_cpu_init();
	page_address_init();
	pr_notice("%s", linux_banner);
	early_security_init();
	/*解析bootargs命令和将设备树转化为platform_device节点*/
	setup_arch(&command_line);
	setup_boot_config(command_line);
	setup_command_line(command_line);
	setup_nr_cpu_ids();
	setup_per_cpu_areas();
	smp_prepare_boot_cpu();	/* arch-specific boot-cpu hooks */
	boot_cpu_hotplug_init();

	...
	
	/*
	 * These use large bootmem allocations and must precede
	 * kmem_cache_init()
	 */
	setup_log_buf(0);
	vfs_caches_init_early();
	sort_main_extable();
	trap_init();
	/*内存管理初始化*/
	mm_init();

	...
	
	/*
	 * HACK ALERT! This is early. We're enabling the console before
	 * we've done PCI setups etc, and console_init() must be aware of
	 * this. But we do want output early, in case something goes wrong.
	 */
	 /*控制台初始化*/
	console_init();
	if (panic_later)
		panic("Too many boot %s vars at `%s'", panic_later,
		      panic_param);

	... 
	
	/* Do the rest non-__init'ed, we're now alive */
	/*
	1. 加载驱动
	2. probe初始化硬件
	3. 挂载根分区
	4. 根据bootargs参数在根分区找init进程
	5. 运行init进程
	*/
	arch_call_rest_init();

	/*kenel 不会运行到这里*/
	prevent_tail_call_optimization();
}
上述流程是kernel启动的几个需要重点知道的流程,对kernel驱动开发有帮助。由与kernel启动流程项目比较多,涉及知识领域广,从不同的角度有着不同的分析方法,本文章是从驱动的角度去分析的kernel。
kernel在启动过成中会解析由uboot带过来的bootargs参数,这些参数是由例入__setup()这一类的宏来完成代码处理的。
举例:
 static int __init selinux_enabled_setup(char *str)
 {
    unsigned long enabled;
    if (!kstrtoul(str, 0, &enabled))
        selinux_enabled_boot = enabled ? 1 : 0;
    return 1;
}
__setup("selinux=", selinux_enabled_setup);
若bootargs=`selinux=1` 则selinux_enabled_setup函数将设置变量selinux_enabled_boot等于1。通常在bootargs中会设置“iniit=/linuxrc console=ttyPS0“ 是在告诉kernel要启动的init进程的名字和控制台为ttyPS0。
最终在kernel_init内核线程中加载驱动并挂载分区启动init进程:
	arch_call_rest_init--> kernel_init(void *unused)
	/*
		1. 驱动加载
		2. 硬件初始化
		3. 分区挂载 kernel_init_freeable-->prepare_namespace-->init_mount(".", "/", NULL, MS_MOVE, NULL);
	*/
	kernel_init_freeable();
	
	...
	
  if (!try_to_run_init_process("/sbin/init") ||
         !try_to_run_init_process("/etc/init") ||
         !try_to_run_init_process("/bin/init") ||
        !try_to_run_init_process("/bin/sh"))
         return 0;

     panic("No working init found.  Try passing init= option to kernel. "
           "See Linux Documentation/admin-guide/init.rst for guidance.");

运行init进程,此处代码是当bootargs未指定init进程的名字或指定init进程名字运行失败时自动默认寻找init进程位置的代码。

另外需要说明的是驱动初始化函数在kernel_init_freeable()符号里面
   kernel_init_freeable()-->do_basic_setup()-->do_initcalls()

init/main.c:

1293 static void __init do_initcalls(void)                                                               
1294 {   
1295     int level;
1296     size_t len = strlen(saved_command_line) + 1;
1297     char *command_line;
1298     
1299     command_line = kzalloc(len, GFP_KERNEL);
1300     if (!command_line)
1301         panic("%s: Failed to allocate %zu bytes\n", __func__, len);
1302 
1303     for (level = 0; level < ARRAY_SIZE(initcall_levels) - 1; level++) {
1304         /* Parser modifies command_line, restore it each time */
1305         strcpy(command_line, saved_command_line);
1306         do_initcall_level(level, command_line);
1307     }
1308 
1309     kfree(command_line);
1310 }  
level为加载驱动的优先级由0到7。
驱动初始化顺序在文件include/linux/init.h中定义

include/linux/module.h:

89 #define module_init(x)  __initcall(x);

include/linux/init.h:

216 #define pure_initcall(fn)       __define_initcall(fn, 0)
217 
218 #define core_initcall(fn)       __define_initcall(fn, 1)
219 #define core_initcall_sync(fn)      __define_initcall(fn, 1s)
220 #define postcore_initcall(fn)       __define_initcall(fn, 2)
221 #define postcore_initcall_sync(fn)  __define_initcall(fn, 2s)
222 #define arch_initcall(fn)       __define_initcall(fn, 3)
223 #define arch_initcall_sync(fn)      __define_initcall(fn, 3s)
224 #define subsys_initcall(fn)     __define_initcall(fn, 4)
225 #define subsys_initcall_sync(fn)    __define_initcall(fn, 4s)
226 #define fs_initcall(fn)         __define_initcall(fn, 5)
227 #define fs_initcall_sync(fn)        __define_initcall(fn, 5s)                                         
228 #define rootfs_initcall(fn)     __define_initcall(fn, rootfs)
229 #define device_initcall(fn)     __define_initcall(fn, 6)
230 #define device_initcall_sync(fn)    __define_initcall(fn, 6s)
231 #define late_initcall(fn)       __define_initcall(fn, 7)
232 #define late_initcall_sync(fn)      __define_initcall(fn, 7s)
233 
234 #define __initcall(fn) device_initcall(fn)
上述代码可以看出我们常用的符号module_init初始化优先级为6
驱动加载成功后会匹配对应的设备树生成的platfrom_device节点,然后初始化硬件。

到此运行init进程开始也就是kernel启动阶段的结束。

3. 文件系统

上面说到init进程被拉起,之后的kernel处于被动状态,需要具体的文件系统内容去操作kernel。针对不同的文件系统工具做的文件系统,它们的应用启动风格又不同。
常见的由initrc风格和busybox风格,busybox是比较早期的文件系统制作工具,initrc是现在大多数操作系统的风格。但是它们的启动流程是相同的,只是启动脚本的格式不同。此处我只作通用部分的说明。

3.1 文件系统的启动

针对busybox的文件系统,当init进程启动成功后,首先是解析/etc/inittab文件,由于文件系统的版本多样化,inittab的语法也由很多不同,此处仅作一些举例说明:
![inittab文件](https://img-blog.csdnimg.cn/ecb9f969dad349ec98ec0c286a89f55e.png)
# Level to run in
id:2:initdefault:
 
# Boot-time system configuration/initialization script.
si::sysinit:/etc/rc.sysinit
 
# What to do in single-user mode.
~:S:wait:/sbin/sulogin
 
# /etc/init.d executes the S and K scripts upon change
# of runlevel.
#
# Runlevel 0 is halt.
# Runlevel 1 is single-user.
# Runlevels 2-5 are multi-user.
# Runlevel 6 is reboot.
 
l0:0:wait:/etc/rc 0
l1:1:wait:/etc/rc 1
l2:2:wait:/etc/rc 2
l3:3:wait:/etc/rc 3
l4:4:wait:/etc/rc 4
l5:5:wait:/etc/rc 5
l6:6:wait:/etc/rc 6
 
# What to do at the "3 finger salute".
ca::ctrlaltdel:/sbin/shutdown -t3 -r now
 
# Runlevel 2,3: getty on virtual consoles
# Runlevel   3: mgetty on terminal (ttyS0) and modem (ttyS1)
1:23:respawn:/sbin/mingetty tty1
2:23:respawn:/sbin/mingetty tty2
3:23:respawn:/sbin/mingetty tty3
4:23:respawn:/sbin/mingetty tty4
S0:3:respawn:/sbin/agetty ttyS0 9600 vt100-nav
S1:3:respawn:/sbin/mgetty -x0 -D ttyS1
当选定运行级别为2时,则会去运行/etc/rc.d/rc2.d脚本,并且传入参数start/stop。/etc/rc.d/rc2.d里面脚本则是软连接到/etc/init.d/里面的启动脚本。(开机传入start,关机传入stop)
lrwxrwxrwx   1 root root    27 23  2023 K01speech-dispatcher -> ../init.d/speech-dispatcher
lrwxrwxrwx   1 root root    15 23  2023 S01acpid -> ../init.d/acpid
lrwxrwxrwx   1 root root    17 23  2023 S01anacron -> ../init.d/anacron
lrwxrwxrwx   1 root root    16 23  2023 S01apport -> ../init.d/apport
lrwxrwxrwx   1 root root    20 23  2023 S01irqbalance -> ../init.d/irqbalance
lrwxrwxrwx   1 root root    20 23  2023 S01kerneloops -> ../init.d/kerneloops
lrwxrwxrwx   1 root root    23 511 22:15 S01open-vm-tools -> ../init.d/open-vm-tools
lrwxrwxrwx   1 root root    17 23  2023 S01openvpn -> ../init.d/openvpn
lrwxrwxrwx   1 root root    18 23  2023 S01plymouth -> ../init.d/plymouth
lrwxrwxrwx   1 root root    37 23  2023 S01pulseaudio-enable-autospawn -> ../init.d/pulseaudio-enable-autospawn
lrwxrwxrwx   1 root root    15 23  2023 S01rsync -> ../init.d/rsync
lrwxrwxrwx   1 root root    17 23  2023 S01rsyslog -> ../init.d/rsyslog
lrwxrwxrwx   1 root root    15 23  2023 S01saned -> ../init.d/saned
lrwxrwxrwx   1 root root    23 23  2023 S01spice-vdagent -> ../init.d/spice-vdagent
lrwxrwxrwx   1 root root    13 23  2023 S01ssh -> ../init.d/ssh

具体执行方式是Init进程执行/etc/init.d/rc 传入运行级别2,然后由/etc/init.d/rc 脚本执行/etc/init.d/rcS,然后循环执行 /etc/rc.d/rc2.d/里面S开头的脚本。
顺着S开头后面的数字依次先后顺序执行下去,就实现了文件系统的启动。

3.2 udev sysfs功能说明

顺着rc2.d里面的脚本的启动,udev sysfs等部件将会顺序被启动或者被挂载。
udev:
是一个应用程序,与驱动进行交互,驱动可实现udev event接口,udev负责接收这些接口上报的事件,并作出相应的处理。
它有以下热点功能
(1)自动创建设备节点,当驱动调用device_create函数时,udev会收到相关事件并在/dev/目录下面创建设备节点;
(2)当出现U盘等热插拔事件时,udev程序将会通过本地套接字,广播设备热插拔等相关字符消息,操作系统的U盘热插拔自动挂载的底层就是依赖udev来实现的。针对这些字符消息的处理,有一些开源包可以方便我们开发。但大多数嵌入式开发是之间接收本地套接子字符串自己解析完成的。
sysfs :
sysfs 是一个与kernel 驱动交互的文件系统,它存在于内存,需要由用户程序调用命令将其挂载到/sys目录下面,至此用户空间可以通过sysfs与kernel驱动交互。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值