韦东山Linux第三篇笔记 - 下
笔记一、Linux 系统的核心机制:用户空间 (User Space) 与 内核空间 (Kernel Space) 的分离
为什么 写驱动比写应用程序难得多?
🔬 概念一:内核空间 vs. 用户空间
要理解驱动和应用的区别,首先要理解 Linux 操作系统是如何划分权力区域的:
| 空间 | 权力 (权限) | 运行的代码类型 | 目的 |
|---|---|---|---|
| 内核空间 | 最高(特权模式),可以直接访问所有硬件寄存器和内存。 | Linux 内核本身、驱动程序 | 管理和控制硬件资源,保证系统稳定。 |
| 用户空间 | 受限(非特权模式),无法直接访问硬件。 | 应用程序、Shell、库函数 (glibc) | 运行用户程序,提供用户界面。 |
💻 概念二:Linux 应用 vs. Linux 驱动
您的 HelloWorld.c 示例,即在虚拟机Linux环境下上写个HelloWorld.c,然后通过交叉编译输出ARM可运行文件HelloWorld,再让开发板运行,打印HelloWorld,就是典型的 Linux 应用。
1. Linux 应用 (User Space Application)
- 定位: 运行在用户空间。
- 功能: 实现业务逻辑。例如,计算、网络连接、显示 LVGL 界面、读取文件等。
- 如何与硬件通信?
- 您的
HelloWorld中printf函数最终会调用内核的 系统调用 (System Call),请求内核来帮忙完成屏幕输出。 - 应用永远不直接操作硬件。 比如,您想点亮 LED,应用会打开
/dev/led文件,然后调用write(),由内核将请求转发给驱动程序。
- 您的
- 依赖: 依赖于标准的 C 库(如 glibc)和操作系统的稳定接口。
2. Linux 驱动 (Kernel Space Driver)
- 定位: 运行在内核空间。
- 功能: 直接控制硬件。 驱动程序是硬件和应用程序之间的翻译官。它接收来自应用的请求(系统调用),然后操作 I.MX6ULL 芯片的寄存器 来完成实际的物理动作(比如拉高 GPIO 电平)。
- 如何与应用通信?
- 它向用户空间提供文件接口 (例如
/dev/xxx),供应用程序调用。
- 它向用户空间提供文件接口 (例如
- 依赖: 它依赖于整个 Linux 内核的内部结构和API。
🧩 概念三:为什么编译驱动程序那么复杂?
驱动程序和应用程序在编译环境上有着根本区别。
1. 驱动程序依赖内核(就像树根依赖土壤)
驱动程序是内核的一部分,它必须知道内核内部的各种数据结构、函数定义、宏定义、中断号等等。这些信息都存储在内核源码的 头文件 (.h) 中。
2. asm/ 文件链接是关键难点
您提到的 asm/ 目录,它代表 Assembly/Architecture Specific(汇编/架构相关)。
- 原因: Linux 内核代码中有一些与硬件架构密切相关的定义(例如,CPU 如何处理内存、如何处理中断)。
- 结构: 为了实现通用性,内核源码中有一个名为
include/asm的目录,它是一个软链接 (Symbolic Link)。 - 动作: 当您配置和编译内核时,构建系统会根据您选择的 ARM 架构 ,将这个
asm软链接指向asm-arm目录。 - 结果: 如果您直接编译驱动,而没有先配置和编译内核,那么
asm这个链接可能指向错误的位置,或者根本不存在。驱动程序在编译时找不到这些架构相关的定义,自然就会失败。
3. 为什么必须先编译内核?
Linux 内核源码里,有成千上万个 .c 文件。当驱动程序想操作 CPU 的底层功能(比如"关闭中断")时,它会引用一个头文件: #include <asm/irq.h>
但是,Linux 支持几十种 CPU(ARM, X86, MIPS...)。
- ARM 的关中断指令在
arch/arm/include/asm/irq.h - X86 的关中断指令在
arch/x86/include/asm/irq.h
代码里的 <asm/irq.h> 到底指的是哪一个?
在你刚下载好的内核源码里,include/asm 这个路径可能根本不存在 ,或者是一个无效的空链接。这时候如果你直接去编译驱动,编译器找 <asm/irq.h> 找不到,直接报错。
当你配置好内核(告诉它"我是 ARM 架构"),并开始编译时,构建系统(Make)会自动做一个动作:
创建一个"快捷方式": 它会在内存或临时目录中,把 include/asm 强行指向 arch/arm/include/asm。
结果:
- 你的驱动程序再次引用
<asm/irq.h>。 - 编译器看到
asm,顺着快捷方式找到了arch/arm/...。 - 编译器成功找到了 ARM 版本的代码。
- 编译成功!
这就是为什么说"必须先编译内核,才能编译驱动"。
因为编译内核不仅仅是生成一个 zImage 文件,更重要的是:
- 生成配置: 运行
make menuconfig会生成.config文件,定义了内核的结构。 - 生成头文件: 构建系统会根据
.config文件,创建出所有正确的、指向特定 ARM 架构的 头文件路径 和 链接结构。
总结: 编译驱动程序,就像是为一栋复杂的房子(内核)定制一个特殊配件。您必须有那栋房子的全套图纸和配置信息(已配置编译的内核源码),才能让配件完美契合。而编译应用程序,则像是在房子的客厅里(用户空间)放一张沙发,不需要知道房子的结构细节。
名词解释
zImage(内核镜像)
- 是什么: 这就是编译好的 Linux 内核可执行文件。
- 类比: 它就像 Windows 里的
C:\Windows\System32\ntoskrnl.exe,或者 STM32 编译出来的.bin/.hex文件。- 特点:
z代表zipped(压缩的)。它是一个经过压缩的内核,开发板启动时会把它解压到内存里运行。
uImage(U-Boot 专用内核镜像)
- 是什么: 它等于
zImage+ 一个头部信息 (Header)。- 作用: U-Boot(启动加载器)有时比较"笨",它不能直接识别原始的
zImage。所以我们需要用一个工具(mkimage)给zImage加个只有 64 字节的"帽子",告诉 U-Boot:"我是内核,加载地址是 xxx,长度是 xxx"。这个戴了帽子的文件就是uImage。- 关系:
uImage=Header+zImage。
make menuconfig(菜单配置)
- 是什么: 这是一个图形化的配置界面(蓝底灰字)。
- 作用: 它就像游戏的"设置"菜单。你可以在这里勾选或取消内核的功能。比如:"我要支持 USB 鼠标"、"我要支持 WiFi"、"我要开启调试模式"。
- 结果: 当你配置完保存退出时,它会生成一个名为
.config的文件。.
.config(配置单)
- 是什么: 一个隐藏的文本文件,里面全是开关。
- 内容示例:
CONFIG_ARM=y(开启ARM支持),CONFIG_WIFI=n(关闭WiFi支持)。- 作用: 编译器(GCC)和构建工具(Make)会读取这个文件,决定哪些代码需要编译,哪些跳过。