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 协调时序。把“数据面”和“控制面”分开设计,接口会更清晰,用户空间也更容易适配。

设备文件与用户空间接口全景 用户空间 应用程序 open/read/write/ioctl udev 动态创建 /dev 节点 sysfs 工具 cat / echo 属性 procfs 工具 调试信息读取 内核空间 VFS 虚拟文件系统层 字符设备驱动 file_operations read / write / ioctl open / release / poll 等待队列 wait_queue sysfs 属性 DEVICE_ATTR_RW show() / store() kobject 层次 uevent 通知 procfs / debugfs proc_create seq_file 接口 调试与统计输出 避免滥用 uevent 数据面:read/write/poll | 控制面:ioctl/sysfs | 节点生成:udev + uevent

综上,设备文件负责“入口”,sysfs/procfs 负责“属性与状态”,ioctl 负责“控制命令”,阻塞与非阻塞负责“时序协调”。四者各司其职,共同构成 Linux 驱动稳定、可观测、可配置的用户空间接口体系。