0x00 前言

完整的做移植也有过3~4次经验了,但是整体的把握,我认为自己还是不熟练,因此把每次的经验记录一下。

0x01 Linux系统组件

一个完整的linux系统,通常包含了Uboot、kernel、**.dtb以及根文件系统, 它们是系统运行需要的4个基础组件。

0x02 U-boot

U-Boot,全称 Universal Boot Loader,是遵循GPL条款的开源项目,U-Boot是 从FADSROM、8xxROM、PPCBOOT逐步发展演化而来的。U-Boot发展至今,已经可以 实现非常多的功能,在操作系统方面,它不仅支持嵌入式Linux系统的引导,还支持NetBSD, VxWorks, QNX, RTEMS, ARTOS, LynxOS, Android等嵌入式操作系统的引导。在CPU架构方面 ,U-Boot支持PowerPC、MIPS、x86、ARM、NIOS、XScale等诸多常用系列的处 理器。U-Boot的主要作用是用来启动操作系统内核,它分为两个阶段,即boot + loade.
boot阶段启动系统,初始化硬件设备,建立内存空间映射图,将系统的 软硬件带到一个合适的状态,
loader阶段将操作系统内核文件加载至内存,之后跳转到内核所在地址运行。
当一个开发板上电的时候,即使是执行非常简单的程序,都需要进行很多初始化的 操作,如初始化时钟、初始化存储控制器、将代码拷贝到运行内存中等操作,大多数 处理器在上电的时候都会有默认的指令执行位置.ARM架构的处理器会从0x00000000地址开始读取第一条指令。
一般来说BootLoader必须提供系统上电时的初始化代码,在系统上电时初始化相关 环境后,BootLoader需要引导完整的操作系统,然后将控制器交给操作系统。 简单来说BootLoader是一段小程序,它在系统上电时执行,通过这段小程序可以将硬件 设备进行初始化,如CPU、SDRAM、Flash、串口、网络等,初始化完毕后调用操作系统内核。 另外,某些BootLoader可能含有一些高级特性,如校验操作系统镜像,从多个操作系统镜像中选择引导合适的操作系统, 甚至可以添加网络功能,让系统自主从网上寻找合适的镜像并且进行引导等等。

阶段

U-Boot启动内核的过程可以分为两个阶段,两个阶段的功能如下:
第一阶段:

  • 硬件设备初始化。
  • 加载U-Boot第二阶段代码到RAM空间。
  • 设置好栈。(硬件)
  • 跳转到第二阶段代码入口。

第二阶段:

  • 初始化本阶段使用的硬件设备。
  • 检测系统内存映射。
  • 将内核从存储区域(Flash、SD Card、eMMC)读取到RAM中。
  • 为内核设置启动参数。
  • 调用内核。

操作

下载模式
在开发的时候,我们可能需要利用一些命令去操作BootLoader,让BootLoader完成一 些事情,我们可以称这种模式为下载模式,比如从内存中的ELF镜像启动,从内存启动 应用程序镜像,使用BOOTP/TFTP协议通过网络启动镜像,打印控制台设备和信息,打印 有关文件系统的信息,列出目录中的文件等等。 当然,下载模式是对于开发人员才有意义。
启动模式
而对于用户则不需要这些操作,直接可以启动 操作系统运行,这种直接启动操作系统的模式可以称之为启动模式

正如我们所说, U-Boot的 功能十分强大,它可以同时支持下载模式与启动模式,并且可以切换模式,它可以在系统上电 时默认等待N秒(N可以随意设置),在这N秒内没有命 令(如按下任意按键、串口发送任意数据等)去操作BootLoader时,U-Boot将启动默认 的操作系统内核(如Linux)。当有命令操作BootLoader时,系统将进入下载模式,在这 个模式下,BootLoader将不会继续启动操作系统内核,而是由开发者去指定BootLoader的工作,如通过串口下 载操作系统镜像,通过网络启动镜像等操作,如下图所示。
总述 - 图1

而当N秒内没有命令去操作BootLoader时,BootLoader将启动默认的操作系统,并将 控制块交给操作系统,显然,在产品发布时BootLoader是工作在启动模式下的,这种模 式是不需要用户通过BootLoader的命令去控制BootLoader,而是直接启动操作系统即可,具体见下图。
总述 - 图2

0x03 Kernel

Kernel即是Linux内核,Linux内核采用宏内核架构,即Linux大部分功能都 会在内核中实现,如进程管理、内存管理、设备管理、文件管理以及网络管 理等功能,它们是运行在内核空间中(也可以称之为特权模式下运行);除 此之外还有一种与宏内核相反的内核架构——微内核,它仅仅是将内核的基本功 能放入内核中 ,如进程管理、进程调度等,而其他的设备管理、文件管理等功能都放在内核 空间之外(即运行在非特权模式下),这种微内核的架构有很优越的扩展性,它 将系统分为各个小的功能模块,把设计难度大大降低。而宏内核的设计架构则没有 非常好的扩展性,但Linux在发展的过程中,很早就引入了内核模块(Loadable Kernel Module,LKM)这一机制,弥补了这一不足之处,内核模块全称为动态可 加载内核模块,就是在内核运行时可以动态加载一组目标代码来实现某些特定的 功能,在这过程中不需要重新编译内核就可以实现动态扩展。模块是具有独立功能 的程序,它可以被单独编译,但不能独立运行。它在运行时被链接到内核作为内核 的一部分在内核空间运行,这与运行在用户空间的进程是不同的。模块通常由一组 函数和数据结构组成,用来实现一种文件系统、一个驱动程序或其他内核上层的功 能,关于内核模块我们后续会讲解。

linux 系统组成

Linux内核主要由5部分组成,分别为:进程管理子系统内存管理子系统文件 子系统网络子系统设备子系统,如下图所示(图片来源网络)。
总述 - 图3
从上图可以看出,整个内核是由5部分组 成,并且由系统调用层(系统调用子系统)进 行统一管理,应用层通过系统调用层的函数接口与内核进 行交互,用户应用程序执行的地方是用户空间,用户空间之下则是 内核空间,Linux 内核正是位于内核空间中,下面分别了解一下内核的5个组成部分。

进程管理子系统

进程管理的核心就是进程的调度。在Linux内核中,进程调度的单元是进程,进程调度控制系统中的多个进程对CPU的访问,从宏观上看,系统 中的进程在CPU中是并发执行的。此外内核通过系统调用提供了应用程序 编程接口,例如:创建新进程(fork,exec),结束进程(kill, exit),并且提供了控制进程,同步进程和进程间通信的接口。
不在此展开可以看我写的这一篇:https://www.yuque.com/0xdaqiao/wzapw0/pegyvc

内存管理子系统

内存管理的主要作用是保证系统安全访问内存区域,且绝大部 分CPU都是支持内存管理单元的(Memory Management Unit,MMU),那么在 Linux中,内存管理子系统就负责管理每个进程完成从虚拟 内存到物理内存的转换,以及系统可用内存空间,此外还顺便 提一点,Linux的2.6版本后引入了对无MMU CPU的支持。
内存管理的硬件按照分页方式管理内**,分页就是把系统的物 理内存按照相同大小等分,每个内存分片称作内存页,通常内存页大小 是4KB。内存管理子系统要管理的不仅是4KB缓冲区,它提供了对4KB缓冲区 的抽象,例如slab分配器。这种内存管理模式使用4KB缓冲区为基数,然 后从中分配管理结构,并跟踪内存页使用情况 ,系统中哪些内存页是满的,哪些内存页面为空,哪些内存页没有完全 使用。这样一来,系统就支持动态调整内存使用情况。除此之外,Linux还 支持内存交换,因为Linux中使用的是虚拟内存,当物理内存不足时,内存管理子系 统会将内存暂时移到磁盘中,在物理内存充裕时又将内存页从磁盘移到物理内存中,这就是内存交换。
一般而言,在32位的系统上,每个进程都最大享有4GB的内存空间,因为由于32位的 系统寻址空间只有
4G**,当然这是虚拟内存,0~3GB是属于用户内存空间,3~4GB是属于 系统内存空间,实际上用户的程序几乎使用不完那么大的用户空间,一旦超出将无法正 常运行,当然系统内存空间与用户内存空间是可以调整的。

文件管理子系统

在Linux系统中有一个重要的概念:一切皆文件,它把一切资源都看 作是文件,包括硬件设备,通常称为设备文件。Linux的文件管理子系 统主要实现了虚拟文件系统(Virtual File System,VFS),虚拟文件系统屏蔽了各种硬件上的差异以及具体实现的细 节,为所有的硬件设备提供统一的接口,这样子也就实现了设备无关性,同时文 件管理系统还为应用层提供统一的API接口。
总的来说,Linux 的文件系统体系结构是对一个对复杂系统进行了抽象化,通过使用一组通用的 API 函数,Linux 可以在许多种存储设备上支持多种文件系 统,如NTFS、EXT2、EXT3、EXT4 、FAT等等;而用户空间包含一些应用程序和 GNU C 库(glibc),它们使用的API接口是由系统调用层提供(如打开、读、写和关闭等),其框 架如下图所示。

总述 - 图4

网络子系统

在Linux 内核中,与网络相关的代码被Linux独立开,形成一个相对独立的子系统,称为网络子系统,
(因为TCP/IP屏蔽了不同操作系统的区别,于是网络子系统就独立出来了吧)
网络子系统是一个层次化的结构,可分为以下几个层次:

  1. Socket 层(也可以称之为协议无关层):Linux 在发展过程中,采 用 BSD Socket API 作为自己的网络相关的 API 接口。同时, Linux 的目标又要能 支持各种不同的协议族,而且这些协议族都可以使用 BSD Socket API 作为应用层的编程接口,这样一来将Socket层抽象出来就能屏蔽不同协议族之间的 差异,不会对应用层的使用产生影响。
  2. 协议层:Linux 网络子系统功能上相当完备,它不仅支持 INET 协议 族(也就是通常所说的 TCP/IP 协议族),而且还支持其它很多种协 议族,如 INET6、DECnet,ROSE,NETBEUI 等,对于 INET 、 INET6 协议族来说,又会进一步将协议族划分为传输层和网络层以及 链路层等,此处就不深入分析。
  3. 网络设备层:网络设备其实是设备驱动层的内容了,它抽象了网卡 数据结构,在一个系统中可能存在多种网卡,屏蔽了不同硬件上的差 异,这一层提供了一组通用函数供底层网络设备驱动程序使用。

总述 - 图5

设备子系统

设备子系统又被称之为设备驱动,在日常生活中,嵌入式中使用的设 备多种多样,如LCD、摄像头、USB、音频等都是属于设备,且设备的厂 商不同其驱动程序也是不同的,但是对于Linux来说,不可能去将每个设备 都包含到内核,它只能抽象去描述某种设备
从前面的章节中可以了解到,系统调用层是Linux内核与应用程序 之间的接口,而设备驱动则是Linux内核与硬件之间的接口,设备驱动程 序为应用程序屏蔽了硬件的细节,这样在应用程序看来,硬件设备只是一个 设备文件,应用程序可以象操作普通文件一样对硬件设备进行操作(打开、读、写和关闭)。设备驱动程 序是内核的一 部分,它主要完成以下的功能:

  1. 对设备初始化和释放
  2. 把数据从内核传送到硬件和从硬件读取数据
  3. 读取应用程序传送给设备文件的数据和回送应用程序请求的数据
  4. 检测和处理设备出现的错误

其实Linux在发展的时候就根据设备的共性将设备分层3大类,分别为:

  • 字符设备
  • 块设备
  • 网络设备

网络设备,它其实就是我们上一章网络子系统中描述的网络 设备层,它其实也是一个抽象,统一描述了不同的网卡设备,如WIFI、以 太网等。因为网络设备存在协议栈(协议族),它涉及了网络层协议,所 以Linux将网络设备单独分层一类设备,网络设备的传输速率通常是很高的。网络设备驱动可以看我这篇:https://www.yuque.com/0xdaqiao/gpm8wd/bpd3w4

字符设备是以字节为单位传输的IO设备,可以提供连续的数据流,应用程 序可以顺序读取,通常不支持随机存取。这种字符传输的效率通常是比较低 的,如鼠标、键盘、串口等都是字符设备,也是一种比较常见的设备。

块设备是以块为单位进行传输的设备,应用程序可以随机访问块设备中的 数据,程序可以指定读取数据的位置。我们的磁盘就是一种常见的块设备,应用 程序可以寻址磁盘上的任何位置,并在这个位置读取数据。不过需要注意的是,块设 备读取的数据只能以块为单位的倍数进行(通常是512Byte的整数倍),而不能与字符设备一样以 字节为单位读取,因此通常来说块设备的传输速度是比较高的。
总述 - 图6

0x04 设备树

在2011年之前,ARM Linux中存在大量描述芯片平台以及板级差 异的垃圾代码,它们位于kernel/arch/arm/plat-xxx目录 和kernel/arch/arm/mach- xxx目录下,用代码描述硬件,如注册platform设备,声明设 备的resource等,但这些代码对于Linux内核来说都是垃圾,因此Linux创始人Linus Torvalds在2011年3月份向Linux社区发送一封邮件,他提出 ARM架构平台应该参考其他平台如PowerPC的设备树机(Device Tree)制 描述硬件,这样子在Linux内核中就不再需要进行大量的冗余编码,许多 硬件及芯片平台的细节可以通过设备树传递给Linux内核
其实设备树是一种描述硬件的数据结构,它把这些硬件设备的信 息,用一个非C语言的脚本语言来描述,而这个脚本文件,就是传 说中的Device Tree(设备树),设备树包括设备树源码(Device Tree Source,DTS)文件、设 备树编译工具(Device Tree Compiler,DTC)与二进制格式设备树(Device Tree Blob,DTB)。DTS包含的头 文件格式为DTSI,DTS文件是一种人类可以看懂的编码格式,由节点(Node)和属 性(Property)组成,节点中又可能会包含子节点,而属性则是可以简单理解为 成对出现的名称与值,如下面的示例。

  1. node1 {
  2. a-string-property = A string”;
  3. a-string-list-property = first string”, second string”;
  4. a-byte-data-property = [0x01 0x23 0x34 0x56];
  5. child-node1 {
  6. first-child-property;
  7. second-child-property = <1>;
  8. a-string-property = Hello, world”;
  9. };

因为Uboot和Linux不能直接识别DTS文件,它们只能识别二进制 文件,所以需要把DTS文件编译成DTB文件,而DTC就是将设备树源 码文件(.dts / .dtsi)编译成二进制格式设备树文件(.dtb)的编译工具,它位于Linux内 核源码的scripts/dtc目录下,在Linux配置中使能了设备树情况下,内核会 自动编译对应的设备树,当然用户也可以单独编译设备树。
DTB可以被内核与BootLoader识别解析,通常在制作NAND Flash、SD Card启动 镜像时,通常会为DTB文件留下一部分存储区域以存储DTB,在BootLoader启 动内核时,会先读取DTB到内存。
Linux的设备树中,可描述的硬件信息包括以下几类:

  1. CPU的数量和类别
  2. 存基地址和大小
  3. 总线和桥
  4. 外设连接
  5. 中断控制器和中断使用情况
  6. GPIO控制器和GPIO使用情况
  7. Clock控制器和Clock使用情况

0x05 根文件系统

它是linux在初始化时 加载的第一个文件系统
根文件系统包括根目录真实文件系统,根文件系统之所以在前面加一个”根”,说明它是加载其它文件系统的”根”,如果没有这个”根”的 话,其它的文件系统也就没有办法进行加载的。因为它包含系统引导和使其他文件系统得以挂载(mount)所必要的文件。根文件系统包函Linux启动时 所必须的目录和关键性的文件,例如Linux启动时必要的初始化文件,它在init目录下,此 外根文件系统中还包括了许多的应用程序bin目录等,任何包括这些Linu x 系统启动所必须的文件都可以成为根文件系统。

在Linux内核启动的初始阶段,首先内核会初始化一个基于内存的文件系统,如initramfs,initrd等,然后以只读的方式去加载根文件系统(load rootfs),读取并 且运行/sbin/init初始化文件,根据/etc/inittab配置文件完成系统的初始化工作(提示:/sbin/init是 一个二进制可执行文件,为系统的初始化程序,而/etc/inittab是它的配置文件),在初始化的过程中,还会以读写的方式重新挂载根文件系统,在系统启动后,根文件系统就可用于存储数据了,存在根文件系统是Linux启动时的必要条件。

在Linux系统中的文件系统目录结构与Windows上有较大的不同。系统中只有 一个根目录,路径是”/”,而其它的分区只是挂载在根目录中的一个文件夹,如”/home”和”/sys”等,这 里的”/”就是Linux中的根目录,因此Linux中只存在一个根目录,在Linux启动后,根目录就位于真实的文件系统中。

Linux中的文件系统多种多样,同时在Linux中一切皆是文件,普通文件、目录、字符 设备、块设备、套接字等都以文件方式被抽象化;且它们需要向上层提供统一的操作接口。虚拟文 件系统VFS就是Linux内核中的一个软件层,向上给用户空间程序提供文件系统操作接口;向下 允许不同的文件系统共存,所以,所有实际文件系统都必须实现VFS的结构封装。 因为无论是访问设备还是需要通过文件系统来访问它的挂载点。