字符设备驱动开发:内核工程ioctl与poll接口实现 - 第1节
1.1 Linux 设备驱动分类
在 Linux 内核中,设备驱动是操作系统与硬件设备之间的桥梁。根据设备类型和交互方式的不同,Linux 设备驱动主要分为以下三大类:
| 驱动类型 | 设备文件类型 | 数据交换方式 | 典型设备 |
|---|---|---|---|
| 字符设备驱动 | 字符设备文件 (c) | 按字节流顺序访问 | 串口、GPIO、I2C、SPI、键盘 |
| 块设备驱动 | 块设备文件 (b) | 按固定大小块随机访问 | 硬盘、eMMC、NAND Flash、SD 卡 |
| 网络设备驱动 | 套接字 (socket) | 数据包收发 | 以太网、WiFi、蓝牙、CAN |
关键区分:字符设备不经过内核缓冲区(无 page cache),直接与用户空间交换数据;块设备通过内核的块 I/O 层和页缓存管理;网络设备通过内核网络协议栈收发数据包。
1.2 字符设备特点
字符设备(Character Device)是 Linux 中最基础、最灵活的驱动类型,具有以下核心特征:
- 顺序字节流:数据按字节流顺序读写,不支持随机访问(无 lseek 偏移量概念)。
- 设备文件:通过
/dev目录下的设备文件与用户空间交互,主设备号标识驱动,次设备号标识具体设备实例。 - 系统调用接口:用户空间通过
open()、close()、read()、write()、ioctl()、poll()等标准系统调用操作设备。 - 无内核缓冲:数据直接在用户缓冲区和设备之间传递,不经过内核页缓存,适合实时性要求高的场景。
- 同步/异步模式:支持阻塞 I/O、非阻塞 I/O、I/O 多路复用(poll/select/epoll)和异步通知(SIGIO)。
工程经验:字符设备驱动是嵌入式系统中最常见的驱动形态。一个典型的嵌入式 Linux 系统可能包含数十个字符设备驱动,覆盖 GPIO、UART、I2C、SPI、PWM、ADC、定时器、看门狗等外设。
1.3 驱动与应用程序交互模型
字符设备驱动与应用程序之间的交互遵循“用户空间 → 系统调用 → 内核驱动 → 硬件”的经典分层模型。理解这一模型是开发高质量驱动的基石。
1.3.1 交互流程
- 应用层发起系统调用:用户程序调用
open()、read()、write()、ioctl()、poll()等标准 C 库函数。 - 系统调用陷入内核:通过软中断(x86 的
int 0x80或 ARM 的svc)从用户态切换到内核态。 - VFS 层分发:虚拟文件系统(VFS)根据设备文件的主设备号找到对应的
file_operations结构体。 - 驱动层执行:调用驱动注册的
open、read、ioctl等回调函数,操作硬件寄存器或管理内核资源。 - 结果返回用户空间:驱动执行完成后,数据通过内核栈或共享缓冲区传回用户空间,系统调用返回。
核心数据结构:
struct file_operations 是字符设备驱动的灵魂。它定义了驱动能够响应的所有系统调用回调函数指针,包括 .owner、.open、.release、.read、.write、.unlocked_ioctl、.poll 等。
1.3.2 file_operations 结构体详解
struct file_operations 定义在 <linux/fs.h> 中,是字符设备驱动的核心接口表。以下是本课程重点关注的成员:
| 成员 | 系统调用 | 功能描述 | 本课程重点 |
|---|---|---|---|
.open |
open() |
打开设备,初始化硬件或分配资源 | 基础 |
.release |
close() |
关闭设备,释放资源 | 基础 |
.read |
read() |
从设备读取数据到用户空间 | 基础 |
.write |
write() |
从用户空间写入数据到设备 | 基础 |
.unlocked_ioctl |
ioctl() |
设备控制命令接口,无需 BKL 锁 | 核心 |
.poll |
poll() / select() / epoll() |
查询设备是否可读/可写,支持 I/O 多路复用 | 核心 |
内核版本注意:在 Linux 2.6.36 之后,
.ioctl 已被移除,统一使用 .unlocked_ioctl(无需大内核锁)和 .compat_ioctl(32位兼容)。本课程所有示例均基于 .unlocked_ioctl 实现。
1.3.3 交互模型代码骨架
以下是一个最小字符设备驱动的 file_operations 注册骨架,展示驱动与应用程序的交互入口:
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#define DEVICE_NAME "my_char_dev"
#define CLASS_NAME "my_class"
static int dev_major;
static struct class *dev_class;
static struct cdev my_cdev;
/* open 回调 */
static int my_open(struct inode *inode, struct file *filp)
{
pr_info("Device opened\n");
return 0;
}
/* release 回调 */
static int my_release(struct inode *inode, struct file *filp)
{
pr_info("Device closed\n");
return 0;
}
/* ioctl 回调 — 本课程核心 */
static long my_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
pr_info("ioctl cmd=0x%x, arg=0x%lx\n", cmd, arg);
return 0;
}
/* poll 回调 — 本课程核心 */
static unsigned int my_poll(struct file *filp, struct poll_table_struct *wait)
{
pr_info("poll called\n");
return POLLIN | POLLRDNORM; /* 始终可读 */
}
/* file_operations 结构体 */
static struct file_operations my_fops = {
.owner = THIS_MODULE,
.open = my_open,
.release = my_release,
.unlocked_ioctl = my_ioctl,
.poll = my_poll,
};
/* 模块初始化 */
static int __init my_init(void)
{
dev_major = register_chrdev(0, DEVICE_NAME, &my_fops);
if (dev_major < 0) {
pr_err("Failed to register device\n");
return dev_major;
}
pr_info("Module loaded, major=%d\n", dev_major);
return 0;
}
/* 模块退出 */
static void __exit my_exit(void)
{
unregister_chrdev(dev_major, DEVICE_NAME);
pr_info("Module unloaded\n");
}
module_init(my_init);
module_exit(my_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Kernel Driver Expert");
MODULE_DESCRIPTION("Minimal char device driver skeleton");
工程要点:上述代码展示了字符设备驱动的最小框架。在实际工程中,
.unlocked_ioctl 和 .poll 的实现需要结合硬件状态、等待队列、自旋锁等内核机制,这正是本课程后续章节要深入讲解的内容。
1.4 本章小结
- Linux 设备驱动分为字符设备、块设备、网络设备三大类,字符设备以字节流顺序访问为特征。
- 字符设备驱动通过
/dev设备文件和file_operations结构体与用户空间交互。 - 驱动与应用程序的交互模型为:用户空间 → 系统调用 → VFS → 驱动回调 → 硬件。
.unlocked_ioctl和.poll是字符设备驱动中实现设备控制和 I/O 多路复用的核心接口,也是本课程的两大主线。