《Linux 设备驱动开发详解:基于最新的 Linux 4.0 内核》 第 3 章 Linux 内核及内核编程

《Linux 设备驱动开发详解:基于最新的 Linux 4.0 内核》

第 3 章 Linux 内核及内核编程

参考:宋宝华 著,机械工业出版社,2015年版


3.1 Linux 内核的发展与演变

3.1.1 Linux 的诞生

Linux 操作系统由芬兰赫尔辛基大学学生 Linus Torvalds 于 1991 年创建。其发展历程是开源软件史上最重要的里程碑之一。

关键历史节点

复制代码
Linux 发展时间线:

1991年8月  Linus 在 comp.os.minix 新闻组发布第一条消息:
           "I'm doing a (free) operating system (just a hobby,
            won't be big and professional like gnu)..."

1991年9月  Linux 0.01 发布(约 10,000 行代码,仅支持 x86)

1992年1月  Linux 0.12 发布,采用 GPL 许可证

1994年3月  Linux 1.0 发布(约 176,250 行代码)
           支持:单处理器 x86,TCP/IP 网络

1996年6月  Linux 2.0 发布
           支持:多处理器(SMP),多种 CPU 架构(Alpha、SPARC、MIPS)

1999年1月  Linux 2.2 发布
           改进:网络性能、SMP 扩展性

2001年1月  Linux 2.4 发布
           支持:USB、PC Card、ISA PnP

2003年12月 Linux 2.6 发布(约 600 万行代码)
           重大改进:O(1) 调度器、抢占式内核、NPTL 线程库

2011年7月  Linux 3.0 发布(版本号变更,无重大架构改变)

2015年4月  Linux 4.0 发布(本书基于此版本)
           新特性:内核热补丁(Live Patching)

2019年3月  Linux 5.0 发布

2022年10月 Linux 6.0 发布

3.1.2 Linux 版本号规则

Linux 内核版本号经历了两个阶段的命名规则:

阶段一:Linux 2.x 时代(稳定版/开发版区分)

复制代码
版本号格式:主版本号.次版本号.修订号

次版本号为偶数 → 稳定版(如 2.6.x)
次版本号为奇数 → 开发版(如 2.5.x)

示例:
  2.6.32  ← 稳定版(次版本号 6 为偶数)
  2.5.75  ← 开发版(次版本号 5 为奇数)

阶段二:Linux 3.x / 4.x 时代(不再区分稳定/开发)

复制代码
版本号格式:主版本号.次版本号[-rcN][-稳定版修订]

  4.0      ← 正式发布版
  4.0-rc7  ← 候选发布版(Release Candidate)
  4.0.1    ← 稳定版修订(bug fix)

内核发布周期:约 9~10 周发布一个新版本
  合并窗口(2周)→ rc1 → rc2 → ... → rc7/rc8 → 正式发布

查看当前内核版本

bash 复制代码
uname -r
# 4.0.0

uname -a
# Linux hostname 4.0.0 #1 SMP Thu Apr 16 12:00:00 UTC 2015 x86_64 GNU/Linux

cat /proc/version
# Linux version 4.0.0 (gcc version 4.9.2) #1 SMP

3.1.3 Linux 内核的许可证

Linux 内核采用 GPL v2(GNU General Public License Version 2) 许可证,这对驱动开发有重要影响:

复制代码
GPL v2 的核心要求:
1. 可以自由使用、复制、修改 Linux 内核
2. 修改后的版本必须以相同的 GPL v2 许可证发布
3. 分发时必须提供源代码(或提供获取源代码的方式)

对驱动开发的影响:
  ✓ 开源驱动(GPL):可以使用所有内核导出符号(EXPORT_SYMBOL + EXPORT_SYMBOL_GPL)
  ✗ 闭源驱动(Proprietary):
      - 只能使用 EXPORT_SYMBOL 导出的符号
      - 不能使用 EXPORT_SYMBOL_GPL 导出的符号
      - 加载时内核打印 "kernel tainted" 警告
      - 内核崩溃时 oops 信息可信度降低

3.2 Linux 内核的组成

3.2.1 Linux 内核的整体架构

Linux 内核是一个宏内核(Monolithic Kernel),所有核心功能运行在同一地址空间(内核空间),相互之间可以直接调用。

复制代码
Linux 内核整体架构:

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
                      用户空间(User Space)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  ┌──────────────────────────────────────────────────────────┐
  │                    系统调用接口(SCI)                     │
  │         open / read / write / ioctl / mmap / fork        │
  └──────────────────────────────────────────────────────────┘
  ┌──────────────┐ ┌──────────────┐ ┌──────────────────────┐
  │   进程管理    │ │   内存管理    │ │      文件系统         │
  │  Process Mgmt│ │  Memory Mgmt │ │   Virtual File System │
  │              │ │              │ │                      │
  │ 进程调度      │ │ 虚拟内存      │ │ ext4/xfs/fat/proc    │
  │ 信号处理      │ │ 内存分配      │ │ sysfs/devtmpfs       │
  │ 进程间通信    │ │ 页面回收      │ │                      │
  └──────────────┘ └──────────────┘ └──────────────────────┘
  ┌──────────────────────────────────────────────────────────┐
  │                    设备驱动(Device Drivers)              │
  │   字符设备 │ 块设备 │ 网络设备 │ USB │ I2C │ SPI │ GPIO  │
  └──────────────────────────────────────────────────────────┘
  ┌──────────────────────────────────────────────────────────┐
  │                    网络子系统(Networking)                │
  │         TCP/IP │ UDP │ IPv6 │ Netfilter │ Socket         │
  └──────────────────────────────────────────────────────────┘
  ┌──────────────────────────────────────────────────────────┐
  │              体系结构相关代码(arch/)                     │
  │         x86 │ ARM │ ARM64 │ MIPS │ PowerPC │ RISC-V     │
  └──────────────────────────────────────────────────────────┘
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
                         硬件层
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

3.2.2 进程管理子系统

进程管理是操作系统的核心功能,Linux 内核的进程管理包括:

(1)进程描述符(task_struct)

Linux 用 task_struct 结构体描述一个进程(线程),它是内核中最重要的数据结构之一:

c 复制代码
/* 定义在 <linux/sched.h>,简化版 */
struct task_struct {
    volatile long   state;        /* 进程状态 */
    void           *stack;        /* 内核栈指针 */
    pid_t           pid;          /* 进程 ID */
    pid_t           tgid;         /* 线程组 ID(主线程的 PID) */
    struct task_struct *parent;   /* 父进程指针 */
    struct list_head children;    /* 子进程链表 */
    struct mm_struct *mm;         /* 内存描述符(用户空间) */
    struct mm_struct *active_mm;  /* 活跃内存描述符 */
    struct files_struct *files;   /* 打开的文件描述符表 */
    struct signal_struct *signal; /* 信号处理 */
    /* ... 还有数百个字段 ... */
};
(2)进程状态
复制代码
Linux 进程状态转换图:

                    fork()
                      │
                      ▼
              ┌───────────────┐
              │  TASK_RUNNING  │ ← 就绪态(等待调度)或运行态
              └───────┬───────┘
                      │ 调度器选中
                      ▼
              ┌───────────────┐
              │   执行中       │
              └───────┬───────┘
          ┌───────────┼───────────┐
          │           │           │
          ▼           ▼           ▼
  ┌──────────────┐ ┌──────────┐ ┌──────────────────┐
  │TASK_INTERRUP-│ │TASK_UNTE-│ │  TASK_STOPPED    │
  │TIBLE(可中断 │ │RRUPTIBLE │ │  (暂停,收到    │
  │  睡眠)      │ │(不可中断 │ │   SIGSTOP)      │
  └──────┬───────┘ │  睡眠)  │ └──────────────────┘
         │         └──────────┘
         │ 等待条件满足/信号
         ▼
  ┌───────────────┐
  │  TASK_RUNNING  │
  └───────────────┘

进程状态说明

状态宏 说明
TASK_RUNNING 0 就绪或正在运行
TASK_INTERRUPTIBLE 1 可中断睡眠(等待资源,可被信号唤醒)
TASK_UNINTERRUPTIBLE 2 不可中断睡眠(等待硬件,不响应信号)
__TASK_STOPPED 4 进程停止(收到 SIGSTOP)
EXIT_ZOMBIE 16 僵尸进程(已退出,等待父进程回收)
(3)Linux 调度器

Linux 4.0 使用 CFS(Completely Fair Scheduler,完全公平调度器) 作为默认调度器:

复制代码
CFS 调度原理:
  - 使用红黑树(rbtree)管理所有可运行进程
  - 每个进程维护 vruntime(虚拟运行时间)
  - 调度器总是选择 vruntime 最小的进程运行
  - vruntime 增长速度与进程优先级(nice值)成反比
  - nice 值范围:-20(最高优先级)~ +19(最低优先级)

实时调度策略(优先级高于 CFS):
  SCHED_FIFO   ← 先进先出实时调度
  SCHED_RR     ← 时间片轮转实时调度
  SCHED_NORMAL ← 普通进程(CFS)

3.2.3 内存管理子系统

内存管理是 Linux 内核最复杂的子系统之一,主要包括:

(1)虚拟内存管理
复制代码
Linux 虚拟内存布局(ARM 32位,4GB 地址空间):

0xFFFFFFFF ┌─────────────────────────────┐
           │   内核空间(1GB)             │
           │   ├── 内核代码/数据           │ 0xC0000000~0xC0FFFFFF
           │   ├── 物理内存直接映射区       │ 0xC0000000~high_memory
           │   ├── vmalloc 区域           │ VMALLOC_START~VMALLOC_END
           │   └── 固定映射区(fixmap)    │
0xC0000000 ├─────────────────────────────┤
           │   用户空间(3GB)             │
           │   ├── 栈(向下增长)          │ 0xBFFFFFFF 附近
           │   ├── 共享库映射区            │
           │   ├── 堆(向上增长)          │
           │   ├── BSS 段(未初始化数据)   │
           │   ├── 数据段(已初始化数据)   │
           │   └── 代码段(只读)          │
0x00000000 └─────────────────────────────┘
(2)内核内存分配

驱动开发中常用的内核内存分配函数:

c 复制代码
#include <linux/slab.h>
#include <linux/vmalloc.h>

/* ── kmalloc:分配物理连续内存(最常用)── */
void *ptr = kmalloc(size, flags);
/*
 * flags 常用值:
 * GFP_KERNEL  ← 可睡眠,用于进程上下文(最常用)
 * GFP_ATOMIC  ← 不可睡眠,用于中断上下文
 * GFP_DMA     ← 分配 DMA 可用内存(物理地址 < 16MB)
 * GFP_NOWAIT  ← 不等待,分配失败立即返回
 */
kfree(ptr);   /* 释放 */

/* ── kzalloc:分配并清零(推荐)── */
void *ptr = kzalloc(size, GFP_KERNEL);
kfree(ptr);

/* ── vmalloc:分配虚拟连续但物理不连续的内存── */
/* 适合分配大块内存(> 128KB),不能用于 DMA */
void *ptr = vmalloc(size);
vfree(ptr);

/* ── 页分配器:直接分配物理页── */
/* order:分配 2^order 个连续物理页 */
struct page *page = alloc_pages(GFP_KERNEL, order);
void *addr = page_address(page);   /* 获取虚拟地址 */
__free_pages(page, order);

/* ── devm_kzalloc:设备管理内存(推荐在驱动中使用)── */
/* 驱动卸载时自动释放,无需手动 kfree */
void *ptr = devm_kzalloc(&pdev->dev, size, GFP_KERNEL);
/* 不需要手动释放 */

各内存分配函数对比

函数 物理连续 可用于 DMA 最大分配 可睡眠
kmalloc 128KB(通常) 取决于 flags
kzalloc 128KB(通常) 取决于 flags
vmalloc 受虚拟地址空间限制
alloc_pages 受物理内存限制 取决于 flags

3.2.4 虚拟文件系统(VFS)

VFS 是 Linux 文件系统的抽象层,为所有文件系统提供统一的接口:

复制代码
VFS 层次结构:

应用程序:open("/etc/passwd", O_RDONLY)
    ↓
VFS:根据路径找到对应的 inode 和 file 对象
    ↓
具体文件系统(ext4/xfs/proc/sysfs/devtmpfs)
    ↓
块设备层(对于磁盘文件系统)或直接返回数据(对于虚拟文件系统)

VFS 核心数据结构

c 复制代码
/* superblock:文件系统的元数据 */
struct super_block {
    dev_t           s_dev;          /* 设备号 */
    unsigned long   s_blocksize;    /* 块大小 */
    struct file_system_type *s_type;/* 文件系统类型 */
    struct super_operations *s_op;  /* 超级块操作函数集 */
    /* ... */
};

/* inode:文件的元数据(不含文件名) */
struct inode {
    umode_t         i_mode;         /* 文件类型和权限 */
    uid_t           i_uid;          /* 所有者 UID */
    gid_t           i_gid;          /* 所有者 GID */
    loff_t          i_size;         /* 文件大小 */
    struct timespec i_atime;        /* 访问时间 */
    struct timespec i_mtime;        /* 修改时间 */
    struct inode_operations *i_op;  /* inode 操作函数集 */
    struct file_operations  *i_fop; /* 文件操作函数集(驱动实现) */
    /* ... */
};

/* file:进程打开文件的实例 */
struct file {
    struct path         f_path;     /* 文件路径 */
    struct inode       *f_inode;    /* 对应的 inode */
    const struct file_operations *f_op; /* 文件操作函数集 */
    loff_t              f_pos;      /* 当前读写位置 */
    void               *private_data; /* 驱动私有数据(常用) */
    /* ... */
};

/* dentry:目录项(文件名到 inode 的映射) */
struct dentry {
    struct inode    *d_inode;       /* 对应的 inode */
    struct dentry   *d_parent;      /* 父目录 */
    struct qstr      d_name;        /* 文件名 */
    /* ... */
};

3.2.5 网络子系统

Linux 网络子系统实现了完整的 TCP/IP 协议栈:

复制代码
Linux 网络协议栈层次:

应用层:socket() / send() / recv()
    ↓
BSD Socket 接口层(AF_INET / AF_UNIX / AF_NETLINK)
    ↓
传输层(TCP / UDP / SCTP)
    ↓
网络层(IPv4 / IPv6 / ICMP / ARP)
    ↓
链路层(以太网帧处理 / Netfilter / Traffic Control)
    ↓
网络设备驱动层(net_device)
    ↓
物理网卡硬件

sk_buff(套接字缓冲区)

sk_buff 是 Linux 网络子系统中最核心的数据结构,用于在各层之间传递网络数据包:

c 复制代码
struct sk_buff {
    struct sk_buff  *next;      /* 链表指针 */
    struct sk_buff  *prev;
    struct net_device *dev;     /* 关联的网络设备 */

    /* 数据指针 */
    unsigned char   *head;      /* 缓冲区起始地址 */
    unsigned char   *data;      /* 有效数据起始地址 */
    unsigned char   *tail;      /* 有效数据结束地址 */
    unsigned char   *end;       /* 缓冲区结束地址 */

    unsigned int    len;        /* 数据长度 */
    /* ... */
};

/*
 * sk_buff 数据区域示意:
 *
 * head                                              end
 *  ↓                                                ↓
 *  ┌──────────┬──────────────────────────┬──────────┐
 *  │ headroom │       有效数据            │ tailroom │
 *  └──────────┴──────────────────────────┴──────────┘
 *              ↑                          ↑
 *             data                       tail
 *
 * 各层协议头部依次添加到 headroom 中(skb_push)
 * 数据从 tailroom 追加(skb_put)
 */

3.2.6 Linux 内核源码目录结构

复制代码
linux-4.0/
├── arch/           ← 体系结构相关代码
│   ├── arm/        ← ARM 架构(含启动代码、中断、MMU)
│   ├── arm64/      ← ARM64(AArch64)架构
│   ├── x86/        ← x86/x86_64 架构
│   └── mips/       ← MIPS 架构
├── block/          ← 块设备层(I/O 调度器)
├── crypto/         ← 加密算法库
├── drivers/        ← 设备驱动(最大目录,约 44% 代码量)
│   ├── char/       ← 字符设备驱动
│   ├── block/      ← 块设备驱动
│   ├── net/        ← 网络设备驱动
│   ├── i2c/        ← I2C 子系统
│   ├── spi/        ← SPI 子系统
│   ├── gpio/       ← GPIO 子系统
│   ├── usb/        ← USB 子系统
│   ├── mmc/        ← MMC/SD 子系统
│   └── mtd/        ← MTD(内存技术设备,Flash)子系统
├── fs/             ← 文件系统
│   ├── ext4/       ← ext4 文件系统
│   ├── proc/       ← /proc 虚拟文件系统
│   └── sysfs/      ← /sys 虚拟文件系统
├── include/        ← 内核头文件
│   ├── linux/      ← 通用内核头文件
│   └── asm-generic/← 体系结构无关的汇编头文件
├── init/           ← 内核初始化(start_kernel)
├── ipc/            ← 进程间通信(信号量、消息队列、共享内存)
├── kernel/         ← 内核核心(调度器、信号、时钟)
├── lib/            ← 内核通用库(字符串、链表、红黑树)
├── mm/             ← 内存管理(页分配、slab、vmalloc)
├── net/            ← 网络协议栈
├── scripts/        ← 编译脚本(Kbuild、Kconfig)
├── security/       ← 安全模块(SELinux、AppArmor)
├── sound/          ← 音频子系统(ALSA)
├── tools/          ← 用户空间工具
├── Kconfig         ← 顶层配置文件
├── Makefile        ← 顶层 Makefile
└── vmlinux.lds.S   ← 内核链接脚本

3.3 Linux 内核空间与用户空间

3.3.1 两种空间的本质区别

Linux 将虚拟地址空间划分为内核空间用户空间,这是 Linux 安全性和稳定性的基础:

复制代码
地址空间划分(ARM 32位):

0xFFFFFFFF ┌─────────────────────────────┐
           │                             │
           │       内核空间(1GB)         │  ← 所有进程共享同一内核空间
           │                             │
0xC0000000 ├─────────────────────────────┤
           │                             │
           │       用户空间(3GB)         │  ← 每个进程有独立的用户空间
           │                             │
0x00000000 └─────────────────────────────┘

注意:
  - 内核空间:所有进程共享,内核代码和驱动运行在此
  - 用户空间:每个进程独立,进程间相互隔离
  - 进程切换时,用户空间地址映射改变,内核空间不变

3.3.2 CPU 特权级

复制代码
x86 架构的 CPU 特权级(Ring):

Ring 0 ← 内核模式(最高特权)
         可执行所有指令,访问所有内存,操作硬件
         Linux 内核、驱动程序运行在此

Ring 1 ← (Linux 不使用)
Ring 2 ← (Linux 不使用)

Ring 3 ← 用户模式(最低特权)
         不能执行特权指令(如 cli、sti、in、out)
         不能直接访问硬件
         应用程序运行在此

ARM 架构对应关系:
  内核模式(SVC/IRQ/FIQ/ABT/UND)← 对应 Ring 0
  用户模式(USR)                 ← 对应 Ring 3

3.3.3 用户空间与内核空间的切换

用户空间程序通过系统调用进入内核空间,这是两个空间之间唯一合法的切换途径:

复制代码
系统调用的完整过程(以 read() 为例,ARM 架构):

用户空间:
  1. 应用程序调用 glibc 的 read() 函数
  2. glibc 将系统调用号(__NR_read = 3)放入 R7 寄存器
  3. 将参数放入 R0、R1、R2 寄存器
  4. 执行 SWI 0(软件中断)指令

CPU 硬件:
  5. 保存当前 CPSR 到 SPSR_svc
  6. 切换到 SVC 模式(内核模式)
  7. 跳转到异常向量表中的 SWI 处理入口

内核空间:
  8. 保存用户寄存器到内核栈
  9. 根据 R7 中的系统调用号,查找系统调用表
  10. 调用对应的内核函数 sys_read()
  11. sys_read() 调用 VFS → 驱动的 read 函数
  12. 将结果放入 R0
  13. 恢复用户寄存器,切换回用户模式
  14. 返回用户空间,继续执行

用户空间:
  15. glibc 的 read() 返回结果给应用程序

系统调用号查看

bash 复制代码
# 查看 ARM 系统调用号
cat /usr/include/arm-linux-gnueabihf/asm/unistd.h | grep "__NR_read"
# #define __NR_read  3

# 查看 x86_64 系统调用号
cat /usr/include/x86_64-linux-gnu/asm/unistd_64.h | grep "read"
# #define __NR_read  0

# 使用 strace 跟踪系统调用
strace ls
# execve("/bin/ls", ["ls"], ...) = 0
# open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
# read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0"..., 832) = 832
# ...

3.3.4 内核空间与用户空间的数据交换

由于两个空间相互隔离,数据交换必须通过专用函数:

c 复制代码
#include <linux/uaccess.h>

/*
 * copy_to_user:内核空间 → 用户空间
 * 参数:to(用户空间目标地址),from(内核空间源地址),n(字节数)
 * 返回:0 表示成功,非0 表示未能复制的字节数
 */
unsigned long copy_to_user(void __user *to, const void *from, unsigned long n);

/*
 * copy_from_user:用户空间 → 内核空间
 * 参数:to(内核空间目标地址),from(用户空间源地址),n(字节数)
 * 返回:0 表示成功,非0 表示未能复制的字节数
 */
unsigned long copy_from_user(void *to, const void __user *from, unsigned long n);

/* 单值传递的简化版本 */
put_user(val, ptr);    /* 内核 → 用户(单个值) */
get_user(val, ptr);    /* 用户 → 内核(单个值) */

/*
 * 为什么不能直接用 memcpy?
 * 1. 用户空间指针可能无效(NULL、未映射、已释放)
 * 2. 直接访问无效指针会导致内核 oops(崩溃)
 * 3. copy_to/from_user 内部会验证指针有效性(access_ok)
 *    如果指针无效,返回错误而不是崩溃
 */

/* 驱动 read 函数的正确写法 */
static ssize_t my_read(struct file *filp, char __user *buf,
                       size_t count, loff_t *ppos)
{
    char kernel_buf[] = "Hello from kernel!";
    int len = strlen(kernel_buf);

    if (count < len)
        return -EINVAL;

    /* 正确:使用 copy_to_user */
    if (copy_to_user(buf, kernel_buf, len))
        return -EFAULT;

    return len;
}

3.3.5 内核线程

内核线程(Kernel Thread)是运行在内核空间的特殊进程,没有用户空间地址映射:

c 复制代码
#include <linux/kthread.h>

static struct task_struct *my_thread;

/* 内核线程函数 */
static int my_thread_func(void *data)
{
    pr_info("内核线程启动,PID = %d\n", current->pid);

    /* 线程主循环 */
    while (!kthread_should_stop()) {
        pr_info("内核线程运行中...\n");
        msleep(1000);   /* 睡眠 1 秒 */
    }

    pr_info("内核线程退出\n");
    return 0;
}

/* 创建并启动内核线程 */
static int __init mydriver_init(void)
{
    my_thread = kthread_run(my_thread_func, NULL, "my_kthread");
    if (IS_ERR(my_thread)) {
        pr_err("创建内核线程失败\n");
        return PTR_ERR(my_thread);
    }
    return 0;
}

/* 停止内核线程 */
static void __exit mydriver_exit(void)
{
    kthread_stop(my_thread);
}

3.4 Linux 内核的编译及加载

3.4.1 内核编译系统(Kbuild)

Linux 内核使用 Kbuild 构建系统,由 Kconfig(配置)和 Makefile(编译)两部分组成。

Kconfig 配置系统

Kconfig 文件定义了内核配置选项,make menuconfig 读取这些文件生成配置界面:

kconfig 复制代码
# drivers/char/Kconfig 示例

config MY_CHAR_DRIVER
    tristate "My Character Device Driver"
    depends on SERIAL_CORE
    select CRC32
    default m
    help
      This driver provides a simple character device interface.

      To compile this driver as a module, choose M here.
      If unsure, say N.

# tristate:可选 Y(编译进内核)、M(编译为模块)、N(不编译)
# bool:只能选 Y 或 N
# depends on:依赖其他配置项
# select:自动选中依赖的配置项
# default:默认值

配置选项的三种状态

bash 复制代码
# make menuconfig 中的三种选择:
[*]  ← Y:编译进内核(Built-in)
[M]  ← M:编译为可加载模块(Module)
[ ]  ← N:不编译

# 对应 .config 文件中的内容:
CONFIG_MY_CHAR_DRIVER=y   ← 编译进内核
CONFIG_MY_CHAR_DRIVER=m   ← 编译为模块
# CONFIG_MY_CHAR_DRIVER is not set  ← 不编译
Kbuild Makefile

每个目录下的 Makefile 告诉 Kbuild 如何编译该目录下的文件:

makefile 复制代码
# drivers/char/Makefile 示例

# 编译进内核的对象(obj-y)
obj-y += mem.o random.o

# 根据配置决定是否编译(obj-$(CONFIG_XXX))
obj-$(CONFIG_MY_CHAR_DRIVER) += my_char_driver.o

# 多文件模块
obj-$(CONFIG_MY_COMPLEX_DRIVER) += my_complex.o
my_complex-objs := file1.o file2.o file3.o

# 子目录
obj-$(CONFIG_USB) += usb/
obj-$(CONFIG_I2C) += i2c/

3.4.2 内核编译步骤

bash 复制代码
# ── 步骤一:配置内核 ──────────────────────────────────────

# 使用默认配置
make defconfig                          # x86 默认配置
make ARCH=arm vexpress_defconfig        # ARM Vexpress 默认配置

# 图形化配置(推荐)
make menuconfig                         # 基于 ncurses 的菜单界面
make xconfig                            # 基于 Qt 的图形界面
make gconfig                            # 基于 GTK 的图形界面

# 基于已有配置修改
make oldconfig                          # 基于旧 .config,只询问新增选项
make olddefconfig                       # 基于旧 .config,新增选项用默认值

# ── 步骤二:编译内核 ──────────────────────────────────────

# 编译所有(内核镜像 + 模块 + 设备树)
make -j$(nproc)

# 只编译内核镜像
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- zImage -j$(nproc)

# 只编译设备树
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- dtbs

# 只编译模块
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- modules -j$(nproc)

# ── 步骤三:安装 ──────────────────────────────────────────

# 安装模块到系统(本机)
sudo make modules_install
# 模块安装到 /lib/modules/$(uname -r)/

# 安装模块到指定目录(交叉编译)
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- \
     INSTALL_MOD_PATH=/path/to/rootfs modules_install

# 安装内核(本机,x86)
sudo make install
# 复制 vmlinuz、System.map、config 到 /boot/
# 更新 GRUB 引导配置

# ── 编译产物 ──────────────────────────────────────────────
ls -lh vmlinux                          # 未压缩内核 ELF 文件(调试用)
ls -lh arch/arm/boot/zImage             # 压缩内核镜像(ARM)
ls -lh arch/x86/boot/bzImage           # 压缩内核镜像(x86)
ls -lh arch/arm/boot/dts/*.dtb         # 设备树二进制文件
ls -lh System.map                       # 内核符号表(调试用)

3.4.3 内核模块的编译

外部模块(Out-of-Tree Module)的 Makefile

makefile 复制代码
# 驱动模块的 Makefile(在内核源码树之外)

# 内核源码路径(本机调试)
KERNEL_DIR ?= /lib/modules/$(shell uname -r)/build

# 交叉编译时指定内核源码路径
# KERNEL_DIR ?= /home/user/linux-4.0

PWD := $(shell pwd)

# 要编译的模块
obj-m := my_driver.o

# 多文件模块
# obj-m := my_driver.o
# my_driver-objs := main.o helper.o irq.o

all:
	$(MAKE) -C $(KERNEL_DIR) M=$(PWD) modules

clean:
	$(MAKE) -C $(KERNEL_DIR) M=$(PWD) clean

# 交叉编译目标
arm:
	$(MAKE) -C $(KERNEL_DIR) M=$(PWD) \
		ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- modules

编译过程分析

bash 复制代码
make
# 输出:
# make -C /lib/modules/4.0.0/build M=/home/user/my_driver modules
# make[1]: Entering directory '/usr/src/linux-4.0'
#   CC [M]  /home/user/my_driver/my_driver.o
#   Building modules, stage 2.
#   MODPOST 1 modules
#   CC      /home/user/my_driver/my_driver.mod.o
#   LD [M]  /home/user/my_driver/my_driver.ko
# make[1]: Leaving directory '/usr/src/linux-4.0'

# 生成的文件说明:
# my_driver.o      ← 编译生成的目标文件
# my_driver.mod.c  ← 模块信息源文件(自动生成)
# my_driver.mod.o  ← 模块信息目标文件
# my_driver.ko     ← 最终的内核模块文件
# Module.symvers   ← 模块导出符号表
# modules.order    ← 模块编译顺序

3.4.4 内核模块的加载与卸载

bash 复制代码
# ── 加载模块 ──────────────────────────────────────────────

# insmod:直接加载,不处理依赖
sudo insmod my_driver.ko
sudo insmod my_driver.ko param1=10 param2="hello"  # 传递参数

# modprobe:智能加载,自动处理依赖(需要先 depmod)
sudo modprobe my_driver
sudo modprobe my_driver param1=10

# ── 卸载模块 ──────────────────────────────────────────────

sudo rmmod my_driver          # 卸载(模块名,不含 .ko)
sudo modprobe -r my_driver    # 卸载并移除不再需要的依赖

# ── 查看模块信息 ──────────────────────────────────────────

lsmod                         # 列出所有已加载模块
lsmod | grep my_driver        # 过滤特定模块

modinfo my_driver.ko          # 查看模块详细信息
# filename:    my_driver.ko
# license:     GPL v2
# author:      ...
# description: ...
# parm:        param1:参数描述 (int)

cat /proc/modules             # 查看模块状态(含内存地址)
ls /sys/module/my_driver/     # 通过 sysfs 查看模块信息

# ── 更新模块依赖数据库 ────────────────────────────────────

sudo depmod -a                # 扫描所有模块,生成 modules.dep
# 模块安装后必须运行 depmod,modprobe 才能找到模块

3.4.5 内核启动过程

了解内核启动过程有助于理解驱动的初始化时机:

复制代码
Linux 内核启动流程(ARM 平台):

1. Bootloader(U-Boot)阶段
   ├── 初始化 DDR、时钟
   ├── 加载内核镜像(zImage)到 DDR
   ├── 加载设备树(.dtb)到 DDR
   └── 跳转到内核入口(arch/arm/boot/compressed/head.S)

2. 内核解压阶段
   └── 解压 zImage → vmlinux,跳转到内核真正入口

3. 体系结构初始化(arch/arm/kernel/head.S)
   ├── 设置 CPU 模式(SVC 模式)
   ├── 初始化 MMU(建立页表)
   ├── 初始化 Cache
   └── 跳转到 start_kernel()

4. start_kernel()(init/main.c)
   ├── setup_arch()          ← 体系结构初始化(解析设备树)
   ├── mm_init()             ← 内存管理初始化
   ├── sched_init()          ← 调度器初始化
   ├── time_init()           ← 时钟初始化
   ├── console_init()        ← 控制台初始化(可以 printk 了)
   ├── rest_init()
   │   ├── kernel_thread(kernel_init)  ← 创建 init 进程(PID=1)
   │   └── cpu_idle()        ← CPU 空闲循环
   └── ...

5. kernel_init()(PID=1)
   ├── do_initcalls()        ← 调用所有 __initcall 注册的函数
   │   ├── 驱动的 module_init 函数在此被调用
   │   └── 按优先级顺序调用(core → postcore → arch → subsys → fs → device → late)
   └── 执行用户空间 init 程序(/sbin/init 或 /init)

6. 用户空间 init(systemd / SysVinit / BusyBox init)
   └── 启动各种系统服务和应用程序

__initcall 优先级

c 复制代码
/* 驱动初始化函数的调用优先级(数字越小越早调用) */
pure_initcall(fn)        /* 优先级 0:纯初始化 */
core_initcall(fn)        /* 优先级 1:核心初始化 */
postcore_initcall(fn)    /* 优先级 2:核心后初始化 */
arch_initcall(fn)        /* 优先级 3:体系结构初始化 */
subsys_initcall(fn)      /* 优先级 4:子系统初始化(总线驱动) */
fs_initcall(fn)          /* 优先级 5:文件系统初始化 */
device_initcall(fn)      /* 优先级 6:设备驱动初始化(module_init 默认) */
late_initcall(fn)        /* 优先级 7:延迟初始化 */

/* module_init 实际上是 device_initcall 的别名 */
#define module_init(fn)  __initcall(fn)  /* 等价于 device_initcall(fn) */

3.5 Linux 内核编程须知

3.5.1 没有 glibc 库

内核代码不能使用 C 标准库(glibc),必须使用内核提供的等价函数:

c 复制代码
/* ── 字符串操作 ──────────────────────────────────────────── */
/* 用户空间(glibc)    →    内核空间 */
strlen(s)              →    strlen(s)          /* 同名,但实现不同 */
strcpy(dst, src)       →    strcpy(dst, src)   /* 内核有自己的实现 */
strncpy(dst, src, n)   →    strncpy(dst, src, n)
strcmp(s1, s2)         →    strcmp(s1, s2)
strcat(dst, src)       →    strcat(dst, src)
sprintf(buf, fmt, ...) →    sprintf(buf, fmt, ...)  /* 内核版本 */
snprintf(buf, n, ...)  →    snprintf(buf, n, ...)

/* ── 内存操作 ──────────────────────────────────────────── */
malloc(size)           →    kmalloc(size, GFP_KERNEL)
calloc(n, size)        →    kzalloc(n*size, GFP_KERNEL)
free(ptr)              →    kfree(ptr)
memcpy(dst, src, n)    →    memcpy(dst, src, n)   /* 内核版本 */
memset(ptr, val, n)    →    memset(ptr, val, n)   /* 内核版本 */

/* ── 输出 ──────────────────────────────────────────────── */
printf("fmt", ...)     →    printk(KERN_INFO "fmt", ...)
fprintf(stderr, ...)   →    pr_err("fmt", ...)

/* ── 文件操作(内核中极少使用)──────────────────────────── */
fopen(path, mode)      →    filp_open(path, flags, mode)
fread/fwrite           →    kernel_read/kernel_write
fclose(fp)             →    filp_close(fp, NULL)

/* ── 时间 ──────────────────────────────────────────────── */
sleep(seconds)         →    msleep(ms) / ssleep(seconds)
usleep(us)             →    usleep_range(min_us, max_us)
gettimeofday()         →    do_gettimeofday() / ktime_get()

3.5.2 不能使用浮点运算

内核代码不能直接使用浮点运算,因为内核不保存/恢复浮点寄存器状态:

c 复制代码
/* ❌ 错误:在内核中直接使用浮点 */
static int my_func(void)
{
    float result = 3.14 * 2.0;   /* 危险!可能破坏用户进程的浮点状态 */
    return (int)result;
}

/* ✅ 正确方法一:使用整数运算代替浮点 */
static int my_func(void)
{
    /* 用定点数:将 3.14 表示为 314,最后除以 100 */
    int result = 314 * 2 / 100;   /* = 6 */
    return result;
}

/* ✅ 正确方法二:如果必须使用浮点,手动保存/恢复 FPU 状态 */
#include <asm/fpu/api.h>

static int my_func(void)
{
    float result;
    kernel_fpu_begin();    /* 保存 FPU 状态 */
    result = 3.14f * 2.0f;
    kernel_fpu_end();      /* 恢复 FPU 状态 */
    return (int)result;
}

3.5.3 内核栈很小

内核栈大小通常只有 4KB 或 8KB(用户空间栈默认 8MB),因此:

c 复制代码
/* ❌ 错误:在内核栈上分配大数组 */
static int my_func(void)
{
    char buf[4096];        /* 危险!可能导致栈溢出 */
    int  array[1024];      /* 危险!4KB 数据直接放栈上 */
    /* ... */
}

/* ✅ 正确:使用动态内存分配 */
static int my_func(void)
{
    char *buf = kmalloc(4096, GFP_KERNEL);
    if (!buf)
        return -ENOMEM;

    /* 使用 buf */

    kfree(buf);
    return 0;
}

/* 内核栈大小查看 */
// cat /proc/sys/kernel/perf_event_max_stack
// 通常为 127(帧数限制)
// 实际栈大小:CONFIG_THREAD_SIZE(通常 8192 字节)

3.5.4 并发与竞态

内核代码运行在多处理器环境中,必须处理并发访问问题:

(1)自旋锁(Spinlock)

适用于中断上下文持锁时间极短的场景:

c 复制代码
#include <linux/spinlock.h>

/* 定义和初始化 */
spinlock_t my_lock;
spin_lock_init(&my_lock);

/* 或者静态初始化 */
DEFINE_SPINLOCK(my_lock);

/* 基本使用 */
spin_lock(&my_lock);
/* 临界区(不能睡眠!) */
spin_unlock(&my_lock);

/* 在中断处理函数中使用(禁止本地中断) */
unsigned long flags;
spin_lock_irqsave(&my_lock, flags);
/* 临界区 */
spin_unlock_irqrestore(&my_lock, flags);

/*
 * 自旋锁规则:
 * 1. 持锁期间不能睡眠(不能调用 msleep、kmalloc(GFP_KERNEL) 等)
 * 2. 持锁期间不能调用可能睡眠的函数
 * 3. 如果中断处理函数也访问同一数据,必须用 spin_lock_irqsave
 */
(2)互斥锁(Mutex)

适用于进程上下文,持锁时间较长的场景:

c 复制代码
#include <linux/mutex.h>

/* 定义和初始化 */
struct mutex my_mutex;
mutex_init(&my_mutex);

/* 或者静态初始化 */
DEFINE_MUTEX(my_mutex);

/* 基本使用 */
mutex_lock(&my_mutex);      /* 获取锁(可能睡眠等待) */
/* 临界区(可以睡眠) */
mutex_unlock(&my_mutex);    /* 释放锁 */

/* 非阻塞尝试获取 */
if (mutex_trylock(&my_mutex)) {
    /* 成功获取锁 */
    mutex_unlock(&my_mutex);
} else {
    /* 锁已被占用,立即返回 */
}

/* 可被信号中断的等待 */
if (mutex_lock_interruptible(&my_mutex)) {
    return -ERESTARTSYS;   /* 被信号中断 */
}
mutex_unlock(&my_mutex);
(3)信号量(Semaphore)
c 复制代码
#include <linux/semaphore.h>

struct semaphore sem;
sema_init(&sem, 1);    /* 初始值为1,相当于互斥锁 */

down(&sem);            /* P 操作(获取,可能睡眠) */
/* 临界区 */
up(&sem);              /* V 操作(释放) */

/* 可被信号中断 */
if (down_interruptible(&sem))
    return -ERESTARTSYS;
up(&sem);
(4)读写锁(RWLock)

适用于读多写少的场景:

c 复制代码
#include <linux/rwlock.h>

rwlock_t my_rwlock;
rwlock_init(&my_rwlock);

/* 读操作(多个读者可以同时持锁) */
read_lock(&my_rwlock);
/* 读临界区 */
read_unlock(&my_rwlock);

/* 写操作(独占,排斥所有读者和写者) */
write_lock(&my_rwlock);
/* 写临界区 */
write_unlock(&my_rwlock);
(5)原子操作

对于简单的整数操作,使用原子变量避免加锁开销:

c 复制代码
#include <linux/atomic.h>

atomic_t counter;
atomic_set(&counter, 0);       /* 设置值 */

atomic_inc(&counter);          /* 原子加1 */
atomic_dec(&counter);          /* 原子减1 */
atomic_add(5, &counter);       /* 原子加5 */
atomic_sub(3, &counter);       /* 原子减3 */

int val = atomic_read(&counter); /* 读取值 */

/* 原子操作并测试结果 */
if (atomic_dec_and_test(&counter)) {
    /* counter 减1后变为0 */
}

/* 64位原子操作 */
atomic64_t counter64;
atomic64_set(&counter64, 0);
atomic64_inc(&counter64);

3.5.5 中断处理

驱动程序经常需要处理硬件中断:

c 复制代码
#include <linux/interrupt.h>

/* 中断处理函数 */
static irqreturn_t my_irq_handler(int irq, void *dev_id)
{
    struct my_device *dev = dev_id;

    /* 读取中断状态寄存器,确认是本设备的中断 */
    u32 status = readl(dev->base + INT_STATUS_REG);
    if (!(status & MY_INT_FLAG))
        return IRQ_NONE;   /* 不是本设备的中断 */

    /* 清除中断标志(写1清除) */
    writel(MY_INT_FLAG, dev->base + INT_STATUS_REG);

    /* 处理中断(中断上下文,不能睡眠!) */
    /* 如果需要做耗时操作,使用工作队列或 tasklet */
    schedule_work(&dev->work);   /* 调度工作队列 */

    return IRQ_HANDLED;
}

/* 申请中断 */
static int my_probe(struct platform_device *pdev)
{
    int irq = platform_get_irq(pdev, 0);   /* 从设备树获取中断号 */

    int ret = request_irq(irq,
                          my_irq_handler,
                          IRQF_SHARED,     /* 共享中断 */
                          "my_device",
                          dev);
    if (ret) {
        dev_err(&pdev->dev, "申请中断 %d 失败\n", irq);
        return ret;
    }
    return 0;
}

/* 释放中断 */
static int my_remove(struct platform_device *pdev)
{
    free_irq(irq, dev);
    return 0;
}

中断上下文的限制

复制代码
中断上下文(Interrupt Context)的限制:
  ✗ 不能睡眠(不能调用 msleep、wait_event 等)
  ✗ 不能调用可能睡眠的函数(kmalloc(GFP_KERNEL)、mutex_lock 等)
  ✗ 不能访问用户空间内存(copy_to/from_user)
  ✓ 可以使用自旋锁
  ✓ 可以使用 kmalloc(GFP_ATOMIC)
  ✓ 可以调度 tasklet 或工作队列处理耗时操作

判断当前是否在中断上下文:
  in_interrupt()   ← 返回非0表示在中断上下文(硬中断或软中断)
  in_irq()         ← 返回非0表示在硬中断上下文
  in_softirq()     ← 返回非0表示在软中断上下文

3.5.6 内核中的延时

c 复制代码
#include <linux/delay.h>

/* ── 忙等待延时(占用 CPU,精度高)── */
ndelay(100);          /* 纳秒级延时(100ns) */
udelay(10);           /* 微秒级延时(10μs),可用于中断上下文 */
mdelay(5);            /* 毫秒级延时(5ms),忙等待,不推荐 */

/* ── 睡眠延时(让出 CPU,不可用于中断上下文)── */
msleep(100);          /* 毫秒级睡眠(100ms),不可被信号中断 */
msleep_interruptible(100);  /* 可被信号中断的毫秒睡眠 */
ssleep(1);            /* 秒级睡眠(1s) */
usleep_range(900, 1100);    /* 微秒范围睡眠(推荐替代 udelay > 10μs)*/

/*
 * 选择原则:
 * < 10μs:使用 udelay(忙等待)
 * 10μs~20ms:使用 usleep_range(睡眠,精度较高)
 * > 20ms:使用 msleep(睡眠)
 * 中断上下文:只能使用 udelay/ndelay/mdelay
 */

3.5.7 内核中的时间

c 复制代码
#include <linux/jiffies.h>
#include <linux/time.h>
#include <linux/ktime.h>

/* ── jiffies:内核时钟节拍计数 ── */
/* HZ:每秒的时钟节拍数(通常 100、250 或 1000) */
unsigned long now = jiffies;

/* 时间比较(处理溢出) */
if (time_after(jiffies, timeout))    /* jiffies > timeout */
if (time_before(jiffies, timeout))   /* jiffies < timeout */

/* jiffies 与时间单位转换 */
unsigned long timeout = jiffies + msecs_to_jiffies(500);  /* 500ms 后超时 */
unsigned long timeout = jiffies + HZ;                      /* 1秒后超时 */
unsigned long ms = jiffies_to_msecs(jiffies);              /* 转换为毫秒 */

/* ── ktime:高精度时间 ── */
ktime_t start = ktime_get();
/* 执行一些操作 */
ktime_t end = ktime_get();
s64 elapsed_ns = ktime_to_ns(ktime_sub(end, start));
pr_info("耗时:%lld ns\n", elapsed_ns);

/* ── 获取当前时间 ── */
struct timespec64 ts;
ktime_get_real_ts64(&ts);
pr_info("当前时间:%lld.%09ld\n", (long long)ts.tv_sec, ts.tv_nsec);

3.5.8 内核中的链表

Linux 内核提供了高效的双向循环链表实现,广泛用于驱动开发:

c 复制代码
#include <linux/list.h>

/* 定义包含链表节点的结构体 */
struct my_device {
    int id;
    char name[32];
    struct list_head list;   /* 内嵌链表节点 */
};

/* 定义链表头 */
LIST_HEAD(device_list);   /* 静态初始化 */
/* 或动态初始化 */
struct list_head device_list;
INIT_LIST_HEAD(&device_list);

/* 添加节点 */
struct my_device *dev = kzalloc(sizeof(*dev), GFP_KERNEL);
dev->id = 1;
strcpy(dev->name, "device1");
list_add(&dev->list, &device_list);        /* 添加到链表头 */
list_add_tail(&dev->list, &device_list);   /* 添加到链表尾 */

/* 遍历链表 */
struct my_device *entry;
list_for_each_entry(entry, &device_list, list) {
    pr_info("设备 ID=%d, 名称=%s\n", entry->id, entry->name);
}

/* 安全遍历(遍历时可以删除节点) */
struct my_device *tmp;
list_for_each_entry_safe(entry, tmp, &device_list, list) {
    if (entry->id == 1) {
        list_del(&entry->list);
        kfree(entry);
    }
}

/* 删除节点 */
list_del(&dev->list);
kfree(dev);

/* 判断链表是否为空 */
if (list_empty(&device_list))
    pr_info("链表为空\n");

3.5.9 container_of

container_of 是内核中最常用的宏之一,通过结构体成员的指针获取包含该成员的结构体指针:

c 复制代码
/*
 * container_of(ptr, type, member)
 * ptr:成员变量的指针
 * type:包含该成员的结构体类型
 * member:成员变量名
 * 返回:指向包含该成员的结构体的指针
 */

struct my_device {
    int id;
    struct list_head list;   /* 链表节点 */
    struct cdev cdev;        /* 字符设备 */
};

/* 在 open 函数中,通过 inode->i_cdev 获取 my_device 指针 */
static int my_open(struct inode *inode, struct file *filp)
{
    struct my_device *dev;

    /* inode->i_cdev 是 struct cdev 类型的指针 */
    /* 通过 container_of 获取包含它的 my_device 结构体指针 */
    dev = container_of(inode->i_cdev, struct my_device, cdev);

    filp->private_data = dev;   /* 保存供其他函数使用 */
    return 0;
}

/*
 * container_of 的实现原理:
 * #define container_of(ptr, type, member) ({          \
 *     const typeof(((type *)0)->member) *__mptr = (ptr); \
 *     (type *)((char *)__mptr - offsetof(type, member)); })
 *
 * 原理:成员地址 - 成员在结构体中的偏移量 = 结构体起始地址
 */

3.5.10 内核调试技术

c 复制代码
/* ── printk 调试(最基本)── */
pr_info("变量值:%d\n", val);
pr_err("错误:%d\n", ret);
dev_info(dev, "设备 %s 初始化完成\n", dev_name(dev));

/* ── 动态调试(Dynamic Debug)── */
pr_debug("调试信息:%s\n", msg);   /* 默认不输出,可动态开启 */
/* 开启方法:
   echo "file my_driver.c +p" > /sys/kernel/debug/dynamic_debug/control
*/

/* ── /proc 接口调试 ── */
#include <linux/proc_fs.h>
static struct proc_dir_entry *proc_entry;

static int my_proc_show(struct seq_file *m, void *v)
{
    seq_printf(m, "驱动状态:运行中\n");
    seq_printf(m, "中断计数:%d\n", irq_count);
    return 0;
}

static int my_proc_open(struct inode *inode, struct file *file)
{
    return single_open(file, my_proc_show, NULL);
}

static const struct file_operations my_proc_fops = {
    .open    = my_proc_open,
    .read    = seq_read,
    .release = single_release,
};

/* 创建 /proc/my_driver 文件 */
proc_entry = proc_create("my_driver", 0444, NULL, &my_proc_fops);

/* 删除 /proc/my_driver 文件 */
proc_remove(proc_entry);

/* ── /sys 接口调试 ── */
/* 通过 sysfs 暴露驱动内部状态,可读写 */
/* 详见第4章设备模型相关内容 */

/* ── OOPS 分析 ── */
/*
 * 当内核发生错误时,会打印 OOPS 信息:
 * Unable to handle kernel NULL pointer dereference at virtual address 00000000
 * PC is at my_driver_read+0x24/0x80 [my_driver]
 * ...
 * Call trace:
 *   my_driver_read+0x24/0x80 [my_driver]
 *   vfs_read+0x8c/0x17c
 *   sys_read+0x44/0x74
 *
 * 使用 addr2line 定位出错代码行:
 * arm-linux-gnueabihf-addr2line -e my_driver.ko 0x24
 */

本章小结

章节 核心知识点 关键 API / 概念
3.1 内核发展与演变 Linux 历史;版本号规则;GPL v2 许可证对驱动的影响 uname -rMODULE_LICENSE
3.2 内核的组成 五大子系统(进程/内存/VFS/网络/驱动);源码目录结构;task_structsk_buff task_structfile_operationssk_buff
3.3 内核空间与用户空间 地址空间划分;CPU 特权级;系统调用切换过程;数据交换 copy_to_usercopy_from_userstrace
3.4 内核编译及加载 Kbuild/Kconfig 系统;内核编译步骤;模块编译 Makefile;内核启动流程;__initcall 优先级 make menuconfiginsmodmodprobedepmod
3.5 内核编程须知 无 glibc;无浮点;小内核栈;并发与锁;中断上下文限制;链表;container_of kmallocspinlockmutexrequest_irqlist_for_each_entry

内核编程的黄金法则

复制代码
1. 永远检查返回值
   if (IS_ERR(ptr)) { return PTR_ERR(ptr); }
   if (ret < 0) { goto err_cleanup; }

2. 申请的资源必须释放(使用 goto 错误处理模式)
   init:  request_irq → ioremap → kmalloc → register_device
   exit:  unregister_device → kfree → iounmap → free_irq

3. 中断上下文不能睡眠
   使用 in_interrupt() 检查当前上下文

4. 访问共享数据必须加锁
   进程上下文用 mutex,中断上下文用 spinlock

5. 用户空间指针必须用 copy_to/from_user 访问
   永远不要直接解引用用户空间指针

6. 内核栈很小,大数据用 kmalloc
   单个函数栈帧不要超过 1KB

7. 使用 devm_ 系列函数简化资源管理
   devm_kzalloc / devm_ioremap / devm_request_irq

参考文献:宋宝华《Linux设备驱动开发详解:基于最新的Linux 4.0内核》,机械工业出版社,2015年