《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 -r、MODULE_LICENSE |
| 3.2 内核的组成 | 五大子系统(进程/内存/VFS/网络/驱动);源码目录结构;task_struct、sk_buff |
task_struct、file_operations、sk_buff |
| 3.3 内核空间与用户空间 | 地址空间划分;CPU 特权级;系统调用切换过程;数据交换 | copy_to_user、copy_from_user、strace |
| 3.4 内核编译及加载 | Kbuild/Kconfig 系统;内核编译步骤;模块编译 Makefile;内核启动流程;__initcall 优先级 |
make menuconfig、insmod、modprobe、depmod |
| 3.5 内核编程须知 | 无 glibc;无浮点;小内核栈;并发与锁;中断上下文限制;链表;container_of |
kmalloc、spinlock、mutex、request_irq、list_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年