韦东山开发手册阅读笔记(六)

11 阅读9分钟

第十六章 移植 Linux 内核

16.1 Linux内核

Linux 内核(Linux Kernel)是操作系统最核心的部分,负责管理计算机硬件资源,并为上层应用程序提供统一的服务接口。它位于硬件和应用程序之间,充当"中间人"的角色。

16.1.1 主要功能

功能模块职责
进程管理调度 CPU 时间,管理进程/线程的创建、执行和终止
内存管理分配和回收内存,实现虚拟内存机制
文件系统管理磁盘存储,提供文件读写接口
设备驱动与硬件设备通信(键盘、鼠标、网卡、显卡等)
网络协议栈处理网络通信,支持 TCP/IP 等协议
安全机制权限控制、用户认证、访问控制

6.1.2 架构特点

┌─────────────────────────────────────┐
│         用户空间 (应用程序)          │
│    (Shell、浏览器、编辑器、游戏等)    │
├─────────────────────────────────────┤
│           系统调用接口               │
├─────────────────────────────────────┤
│         Linux 内核空间               │
│  ┌─────────┐ ┌─────────┐ ┌────────┐ │
│  │进程管理 │ │内存管理 │ │文件系统│ │
│  ├─────────┤ ├─────────┤ ├────────┤ │
│  │设备驱动 │ │网络协议 │ │安全模块│ │
│  └─────────┘ └─────────┘ └────────┘ │
├─────────────────────────────────────┤
│           硬件层 (CPU、内存、设备)     │
└─────────────────────────────────────┘

16.1.3 Linux-6.19 目录

image.png

16.2 Linux 内核移植的主要步骤

┌─────────────────────────────────────────────────────────┐
│  1. 准备阶段                                              │
│     ├── 获取内核源码 (kernel.org 或芯片厂商仓库)           │
│     ├── 安装交叉编译工具链                                  │
│     └── 准备目标板硬件资料 (SOC datasheet、原理图)          │
├─────────────────────────────────────────────────────────┤
│  2. 配置阶段                                              │
│     ├── 选择参考配置 (arch/arm/configs/xxx_defconfig)      │
│     ├── 使用 menuconfig 进行详细配置                        │
│     └── 关键配置: 处理器架构、驱动支持、文件系统              │
├─────────────────────────────────────────────────────────┤
│  3. 修改与适配阶段                                         │
│     ├── 修改设备树 (Device Tree) - 描述硬件信息              │
│     ├── 添加/修改板级支持代码 (arch/arm/mach-xxx/)          │
│     └── 移植/调试设备驱动                                    │
├─────────────────────────────────────────────────────────┤
│  4. 编译阶段                                              │
│     ├── make zImage/uImage (内核镜像)                       │
│     ├── make dtbs (设备树二进制文件)                         │
│     └── make modules (内核模块)                              │
├─────────────────────────────────────────────────────────┤
│  5. 部署与调试阶段                                         │
│     ├── 烧录内核镜像到存储介质                               │
│     ├── 配置 bootargs 启动参数                              │
│     └── 调试启动过程 (串口打印)                               │
└─────────────────────────────────────────────────────────┘

核心认知:内核移植 90% 的工作是“配参数” ,不是“写代码”。把硬件信息准确告诉内核,内核自带的通用驱动就能跑起来。

上电 → BootROM → U-Boot (第一阶段)
                ↓
         ┌──────────────────┐
         │  初始化 DDR       │
         │  初始化网卡/串口    │
         │  从存储介质加载内核 │
         │  设置 bootargs    │
         │  加载设备树(dtb)  │
         └──────────────────┘
                ↓
         跳转到内核入口点
                ↓
         Linux 内核启动
                ↓
         挂载根文件系统
                ↓
         进入用户空间

16.2.1 与 U-Boot 的关键联系

联系点说明
启动链关系U-Boot → Linux 内核,U-Boot 是内核的"引导者"
镜像加载U-Boot 通过 bootm/bootz 命令加载内核镜像
参数传递U-Boot 通过 bootargs 向内核传递启动参数
设备树支持U-Boot 加载 dtb 文件并传递给内核
硬件初始化U-Boot 完成基础硬件初始化,内核在此基础上运行

U-Boot 负责“把内核搬进内存 + 递上硬件说明书”,Kernel 负责“按说明书初始化硬件 + 挂载文件系统”。
移植内核不是重写驱动,而是精准配置参数、保证 U-Boot 与 Kernel 的握手协议一致

第17章 构建Linux根文件系统

17.1 Linux 文件系统概述

文件系统是操作系统用于管理和存储数据的机制,它定义了数据如何组织、存储和访问。

在 Windows 里,C盘、D盘是平行的,互相独立。但在 Linux 里,并没有C、D、E等盘符的概念,它以树状结构管理所有目录、文件,这个目录被称为挂接点或安装点(mount point),然后就可以通过这个目录来访问这个分区上的文件了。(挂载完成后,进入这个空目录,就等于进入了那个物理存储分区。)

比如根文件系统被挂接在根目录“/”上后,在根目录下就有根文件系统的各个目录、文件:/bin、/sbin、/mnt等;再将其他分区挂接到/mnt目录上, /mnt 目录下就有这个分区的各个目录、文件。而且起点只有一个,那就是根目录(用 / 表示) 。所有的硬盘、U盘、网络存储,都只能作为这棵树的“树枝”接入。

根文件系统的本质:系统运行的"最小依赖集合",其他分区/设备都是"可选挂载",根文件系统是"必须存在"

根文件系统 (Root File System):它是 Linux 内核启动后,强制挂载的第一个文件系统。包含这些必需、基本的文件:

  • init 进程(内核启动后运行的第一个程序)。

  • Shell 程序(提供命令行的黑框框交互界面)。

  • 基础依赖库(如 glibc,供其他程序调用)。

文件系统:其他分区上所有目录、文件的集合。在阅读技术文档时,必须根据语境区分“文件系统”到底在指代什么:

  • 指“数据集合”(实物实体)

    • 指的是存放在某个分区上的所有目录和文件的总和。
  • 指“存储格式”(Type/规范)(文件系统类型 (File System Type)) :

    • 指的是数据在物理介质上存放的排列规则。常见的有针对普通磁盘的 ext2/ext3/ext4,针对 Windows 兼容的 fat32/ntfs,针对嵌入式裸片 Flash 的 yaffs/jffs2

虚拟文件系统 (Virtual File System)

  • 实体文件系统:真实把数据写死在物理硬盘或 Flash 芯片上的结构(如 yaffs, ext3),掉电数据不丢失。

  • 虚拟文件系统 (Virtual File System) :如 /proc/sys 目录。

    • 它们不占用任何实际的磁盘空间
    • 它们是内核在内存 (RAM) 中凭空捏造、动态生成的。
    • 作用:作为内核向用户展示底层状态的“全息投影台”。比如读取 /proc/uptime 文件,其实是触发了内核的计算函数,实时返回当前的开机时间,每次读都不一样。 Linux 文件属性: Linux 系统有如表17.5所示的几种文件类型。

image.png

17.2 FHS目录结构思想

Linux 根文件系统目录结构:

Linux中所有的目录不能乱建。无论是服务器还是嵌入式开发板,都建议遵循FHS标准(Filesystem Hierarchy Standard,文件系统层次标准)

目录核心作用嵌入式特别注意
/bin基本用户命令(ls, cp, cat)必须跟根文件系统在同一分区
/sbin系统管理命令(fdisk, reboot)启动修复时需要
/lib核心共享库 + 内核模块动态库必须在此
/etc配置文件inittab, fstab, init.d/rcS
/dev设备文件现代用udev/systemd,嵌入式用mdev或devtmpfs
/proc内核运行时信息(虚拟文件系统)调试神器:cat /proc/cpuinfo
/sys设备驱动信息(sysfs)现代设备管理基础
/tmp临时文件必须可写,建议挂载tmpfs
/mnt临时挂载点挂载U盘/网络文件系统
/usr只读的用户程序可单独分区,嵌入式常精简
/bin + /sbin + /lib  = 系统启动最小集合(必须在根分区)
/usr                 = 可共享的只读程序(可网络挂载)
/var                 = 可变数据(日志、缓存)
/tmp                 = 临时数据(内存文件系统)

现代实践:嵌入式系统通常把 /usr 合并到根分区,用 tmpfs 挂载 /tmp/var/run 减少Flash写入

17.3 Busybox原理

在桌面版 Ubuntu 里,/bin 下的 lscpmv 是几百个独立的可执行文件,占用几十到上百 MB 的存储空间。 但在嵌入式系统里,Flash 空间极其昂贵;

Busybox 的解决方案: 它把这几百个常用的 Linux 命令,用极其精简的 C 代码重新实现了一遍,并且全部打包成唯一的、极小的可执行文件

文件系统视图

/bin/busybox          ← 唯一真实二进制(几百KB~1MB)
/bin/ls -> busybox     ← 软链接
/bin/cp -> busybox
/bin/cat -> busybox
/sbin/init -> busybox  ← 关键:init也是busybox
/sbin/ifconfig -> busybox

17.4 Init进程启动流程

init 进程:

是由内核启动的第一个(也是惟一的一个)用户进程(进程ID为1),它根据配置文件决定启动哪些程序,比如执行某些脚本、启动shell、运行用户指定的程序等。

init进程是后续所有进程的发起者 (前面的 Bootloader(U-Boot)和 Linux Kernel 做了那么多脏活累活(初始化内存、接管中断、挂载文件系统) ,全都是为了这最后一刻:把接力棒交到用户态的手里。),比如init进程启动/bin/sh程序后,才能够在控制台上输入各种命令。

  • 唯一职责:解析配置文件 → 按需创建/监控子进程(不是干活的,是派活的)
  • 工控机/边缘设备:Busybox init(轻量、可裁剪)
  • 复杂系统:systemd(功能强、依赖多)

启动链(从内核到shell)

内核启动
    ↓
挂载根文件系统
    ↓
执行 /sbin/init(Busybox init)
    ↓
    ├─ 执行 sysinit 动作(/etc/init.d/rcS)
    │       └─ 配置网络、挂载其他文件系统
    │
    ├─ 执行 wait 动作(等待完成)
    │
    ├─ 执行 once 动作(一次性任务)
    │
    └─ 启动 respawn/askfirst 进程(shell/login)
            └─ ttySAC0::askfirst:-/bin/sh
                (串口控制台,按回车启动shell)

17.5 动态库依赖管理

根文件系统最小库集合

/lib/
├── ld-linux.so.3       ← 动态链接器/加载器(必须第一个存在)
├── libc.so.6           ← C标准库(几乎所有程序需要)
├── libm.so.6           ← 数学库(部分程序需要)
├── libgcc_s.so.1       ← GCC运行时(C++程序需要)
└── libpthread.so.0     ← 线程库(多线程程序需要)

动态库核心概念:静态 vs 动态

特性静态链接动态链接
链接时机编译时运行时
库代码位置在可执行文件内部在独立的.so文件中
文件体积大(包含所有库代码)小(仅含引用信息)
内存占用多份拷贝(每个程序一份)共享一份(内存中只加载一次)
更新维护需重新编译程序替换.so文件即可
启动速度快(无需查找库)稍慢(需动态加载解析)

对比示例

# 源代码:hello.c
#include <stdio.h>
int main() {
    printf("Hello\n");
    return 0;
}

# 静态编译
arm-linux-gcc hello.c -o hello_static -static
# 结果:约 500KB(包含完整的libc代码)

# 动态编译(默认)
arm-linux-gcc hello.c -o hello_dynamic
# 结果:约 10KB(仅代码,printf调用外部libc)

交叉编译工具链的组成架构

完整工具链 = 编译器 + 二进制工具 + C库 + 内核头文件

binutils(二进制工具)
├── as      汇编器(.s → .o)
├── ld      链接器(.o → 可执行文件)
├── objdump 反汇编/查看目标文件
├── objcopy 格式转换(生成bin/srec)
├── readelf 解析ELF格式
└── strip   去除符号表(减小体积)

gcc(编译器集合)
├── gcc     C编译器
├── g++     C++编译器
└── 内部调用:预处理器 → 编译器 → 汇编器 → 链接器

C库(二选一)
├── glibc   GNU C库(功能全,体积大)
├── musl    轻量级C库(体积小,静态链接优)
└── uclibc  已逐渐被淘汰

gdb(调试器)
└── arm-linux-gdb 远程调试目标程序