Linux GPIO设备驱动开发与设备树适配 - 第2节
2.1 Linux 内核架构全景
Linux 内核是一个宏内核(Monolithic Kernel),但吸收了微内核的模块化设计思想。理解内核的整体架构是驱动开发的基石。下图展示了内核的核心组件及其相互关系:
核心要点:驱动开发主要工作在内核空间的“设备驱动”和“抽象层”模块,通过系统调用接口为用户空间提供服务。
2.2 内核空间与用户空间
2.2.1 地址空间划分
现代操作系统利用 CPU 的 MMU(内存管理单元)将虚拟地址空间划分为两个区域:用户空间和内核空间。这种隔离机制是系统安全性和稳定性的基石。
| 对比维度 | 用户空间 | 内核空间 |
|---|---|---|
| 特权级别 | Ring 3(非特权模式) | Ring 0(最高特权模式) |
| 地址范围(32位) | 0x00000000 – 0xBFFFFFFF(3GB) | 0xC0000000 – 0xFFFFFFFF(1GB) |
| 可访问硬件 | 不能直接访问,必须通过系统调用 | 可直接访问所有硬件资源 |
| 崩溃影响 | 仅影响当前进程 | 导致整个系统崩溃(内核恐慌) |
| 内存管理 | 使用虚拟地址,可被换出 | 常驻物理内存,不可换出 |
| 开发语言 | C/C++、Python、Java 等 | C 语言 + 少量汇编 |
驱动开发者须知:内核空间代码运行在 Ring 0 级别,拥有最高权限。一个指针错误就可能导致系统崩溃。因此内核编程对内存安全、并发控制和错误处理的要求远高于用户空间编程。
2.2.2 空间切换与上下文
当用户程序需要执行特权操作(如读写文件、创建进程、控制 GPIO)时,必须通过系统调用陷入内核。这个过程称为“上下文切换”:
性能陷阱:在 GPIO 驱动设计中,如果用户程序频繁通过系统调用操作 GPIO(如每毫秒一次),上下文切换开销将远大于 GPIO 操作本身。解决方案是使用内核定时器或中断在驱动内部完成高频操作,仅在必要时与用户空间通信。
2.3 系统调用接口
2.3.1 系统调用机制
系统调用是用户空间访问内核服务的唯一合法入口。Linux 提供了约 300-400 个系统调用(具体数量取决于架构和内核版本)。驱动开发者最常接触的系统调用包括:
- 文件操作类:open()、close()、read()、write()、ioctl()
- 内存管理类:mmap()、munmap()、brk()
- 进程控制类:fork()、execve()、wait()
- 设备控制类:ioctl()(驱动控制的核心接口)
2.3.2 系统调用流程
从用户程序调用到驱动函数执行,经历了以下关键步骤:
- 用户态调用:应用程序调用标准 C 库函数(如 ioctl())
- 库函数封装:glibc 将参数打包,触发软中断(int 0x80 或 syscall 指令)
- 内核入口:CPU 切换到 Ring 0,跳转到内核的 system_call 入口点
- 系统调用分发:根据系统调用号查找 sys_call_table,调用对应的内核函数
- VFS 层处理:对于设备文件,VFS 调用该设备驱动注册的 file_operations 结构体中的函数
- 驱动执行:驱动函数执行实际硬件操作(如读写 GPIO 寄存器)
- 结果返回:逐层返回,最终回到用户空间
2.3.3 驱动中的系统调用实现
在 GPIO 驱动中,最核心的系统调用接口是 ioctl()。驱动开发者需要在 file_operations 结构体中注册对应的回调函数:
// 驱动中定义 ioctl 命令码
#define GPIO_IOC_MAGIC 'G'
#define GPIO_SET_DIR _IOW(GPIO_IOC_MAGIC, 1, int)
#define GPIO_SET_VALUE _IOW(GPIO_IOC_MAGIC, 2, int)
#define GPIO_GET_VALUE _IOR(GPIO_IOC_MAGIC, 3, int)
// 驱动 ioctl 实现
static long gpio_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
int value;
struct gpio_dev *dev = filp->private_data;
switch (cmd) {
case GPIO_SET_DIR:
if (copy_from_user(&value, (int __user *)arg, sizeof(value)))
return -EFAULT;
gpio_set_direction(dev->gpio_num, value);
break;
case GPIO_SET_VALUE:
if (copy_from_user(&value, (int __user *)arg, sizeof(value)))
return -EFAULT;
gpio_set_value(dev->gpio_num, value);
break;
case GPIO_GET_VALUE:
value = gpio_get_value(dev->gpio_num);
if (copy_to_user((int __user *)arg, &value, sizeof(value)))
return -EFAULT;
break;
default:
return -ENOTTY;
}
return 0;
}
// 注册 file_operations
static const struct file_operations gpio_fops = {
.owner = THIS_MODULE,
.unlocked_ioctl = gpio_ioctl,
.open = gpio_open,
.release = gpio_release,
};
关键安全准则:内核空间与用户空间之间传递数据时,必须使用
copy_from_user() 和 copy_to_user() 函数。直接解引用用户空间指针会导致严重的安全漏洞和内核崩溃。
2.4 本章知识结构总结
| 知识点 | 核心内容 | 驱动开发关联 |
|---|---|---|
| 内核架构 | 宏内核 + 模块化设计,核心子系统包括进程管理、内存管理、文件系统、网络栈、设备驱动 | 驱动属于设备驱动子系统,依赖其他子系统提供的 API |
| 内核空间 vs 用户空间 | Ring 0 vs Ring 3,地址隔离,权限差异,上下文切换开销 | 驱动运行在 Ring 0,必须谨慎处理内存和并发 |
| 系统调用接口 | 用户空间访问内核的唯一入口,通过软中断陷入内核 | 驱动通过 file_operations 结构体注册回调,响应系统调用 |
| 数据拷贝 | copy_from_user / copy_to_user 确保安全传输 | 所有用户空间数据必须经过拷贝,禁止直接解引用 |