4. 设备文件与用户空间接口
Linux 驱动对用户空间暴露能力,最终都要落到“文件”这一抽象上。用户程序通过 /dev 下的设备节点、/sys 与 /proc 下的属性文件,以及 ioctl、阻塞/非阻塞读写等接口,与内核驱动交互。理解这些接口的生成方式、数据流向和语义差异,是编写可维护驱动的关键。
一、设备文件:mknod 与 udev 的分工
设备文件是用户空间访问字符设备与块设备的入口。传统方式使用 mknod 手工创建设备节点,需要指定设备类型、主设备号和次设备号:
mknod /dev/mychar c 240 0
ls -l /dev/mychar
# crw-r--r-- 1 root root 240, 0 ... /dev/mychar
其中 c 表示字符设备,240 是主设备号,0 是次设备号。手工创建的问题是设备号必须与驱动注册时一致,且节点不会随设备热插拔自动增删。现代系统由 udev 接管:驱动注册设备后,内核通过 sysfs 与 uevent 通知用户空间,udev 依据规则在 /dev 下动态创建节点,并设置权限、属主和符号链接。
alloc_chrdev_region() 动态申请设备号,避免硬编码主设备号;节点创建交给 udev,驱动只需保证 cdev 与设备号正确注册。二、sysfs 与 procfs:属性与状态的窗口
sysfs 挂载在 /sys,以 kobject 为骨架,把设备、驱动、总线、类等内核对象组织成层次目录。它适合暴露“属性”:设备 ID、状态、可调参数。驱动通过 DEVICE_ATTR 或 device_create_file() 创建属性文件,用户 cat 或 echo 即触发 show()/store() 回调。
static ssize_t mode_show(struct device *dev,
struct device_attribute *attr, char *buf)
{
return sprintf(buf, "%d\n", current_mode);
}
static ssize_t mode_store(struct device *dev,
struct device_attribute *attr,
const char *buf, size_t count)
{
sscanf(buf, "%d", ¤t_mode);
return count;
}
static DEVICE_ATTR_RW(mode);
procfs 挂载在 /proc,更偏向进程与系统运行信息,也可用于调试输出。它接口简单,但缺乏 sysfs 那样的对象模型约束,新驱动应优先使用 sysfs 或 debugfs 暴露调试信息,避免滥用 procfs。
三、ioctl:结构化命令通道
读写接口适合传数据流,而配置、查询、触发类操作更适合 ioctl。ioctl 命令号不是随意整数,而是由方向、类型、序号和大小编码而成,通常用 _IO、_IOR、_IOW、_IOWR 宏定义:
#define MYCHAR_MAGIC 'k'
#define MYCHAR_RESET _IO(MYCHAR_MAGIC, 0)
#define MYCHAR_GET_REG _IOR(MYCHAR_MAGIC, 1, int)
#define MYCHAR_SET_REG _IOW(MYCHAR_MAGIC, 2, int)
long mychar_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
int val;
switch (cmd) {
case MYCHAR_RESET:
reset_device();
break;
case MYCHAR_GET_REG:
val = read_reg();
if (copy_to_user((int __user *)arg, &val, sizeof(val)))
return -EFAULT;
break;
case MYCHAR_SET_REG:
if (copy_from_user(&val, (int __user *)arg, sizeof(val)))
return -EFAULT;
write_reg(val);
break;
default:
return -ENOTTY;
}
return 0;
}
要点是:命令号必须唯一且带类型标识;用户指针必须用 copy_to_user()/copy_from_user() 访问;未知命令返回 -ENOTTY。这样用户空间与驱动共享同一份命令定义头文件,接口才稳定。
四、阻塞与非阻塞 IO
当设备数据未就绪时,驱动有两种选择:让进程睡眠等待(阻塞),或立即返回 -EAGAIN(非阻塞)。用户通过 open() 的 O_NONBLOCK 标志表达意图,驱动用 filp->f_flags 判断。
if (filp->f_flags & O_NONBLOCK) {
if (!data_ready)
return -EAGAIN;
} else {
if (wait_event_interruptible(wq, data_ready))
return -ERESTARTSYS;
}
阻塞实现依赖等待队列:读不到数据时调用 wait_event_interruptible() 睡眠,中断处理或写操作在数据就绪后调用 wake_up_interruptible() 唤醒。非阻塞则配合 poll()/select() 实现多路复用,驱动需实现 .poll 回调并返回 POLLIN、POLLOUT 等掩码。
-ERESTARTSYS;非阻塞时仍睡眠。这些都会导致系统卡顿或用户程序无法预期地挂起。五、接口对比与选择
| 接口 | 典型路径 | 适用场景 | 关键注意点 |
|---|---|---|---|
| 设备节点 | /dev/xxx | 字符/块设备读写 | 主次设备号、udev 规则 |
| sysfs | /sys/... | 属性、状态、参数 | 一属性一文件,show/store 成对 |
| procfs | /proc/... | 调试、运行信息 | 避免滥用,优先 debugfs |
| ioctl | 设备节点 | 配置、查询、触发 | 命令号编码、copy_*_user |
| 阻塞/非阻塞 | read/write/poll | 数据流同步 | 等待队列、O_NONBLOCK、poll |
实际驱动往往组合使用:用 sysfs 暴露配置,用 ioctl 执行控制命令,用 read/write 传输数据,并用阻塞或 poll 协调时序。把“数据面”和“控制面”分开设计,接口会更清晰,用户空间也更容易适配。
综上,设备文件负责“入口”,sysfs/procfs 负责“属性与状态”,ioctl 负责“控制命令”,阻塞与非阻塞负责“时序协调”。四者各司其职,共同构成 Linux 驱动稳定、可观测、可配置的用户空间接口体系。