1、字符设备驱动概述

大家好,我是你们的Linux内核驱动讲师。今天咱们来聊聊字符设备驱动。说实话,我刚开始接触Linux驱动时,也被各种设备分类搞得晕头转向。但别担心,跟着我一步步来,你会发现其实没那么复杂。

1.1 Linux设备驱动分类

Linux系统把设备驱动分成了三大类。嗯,这个分类其实挺直观的,你想想看:

  • 字符设备驱动:按字节流访问,像串口、键盘、鼠标这些
  • 块设备驱动:按数据块访问,比如硬盘、U盘、SD卡
  • 网络设备驱动:处理网络数据包,像网卡、WiFi模块

我个人习惯把这三类设备比作三种不同的工作方式:字符设备就像水管,数据一滴一滴地流;块设备像仓库,数据一箱一箱地搬;网络设备像邮局,数据打包发送。这个比喻虽然不完美,但初学者理解起来会轻松很多。

核心要点:在Linux中,一切皆文件。设备也是文件,驱动就是让这些"设备文件"能正常工作的一套程序。

1.2 字符设备与块设备的区别

我在项目中遇到过不少新手把字符设备和块设备搞混。其实它们的区别很明显,我整理了一个表格:

对比项 字符设备 块设备
数据访问方式 按字节流顺序访问 按数据块随机访问
缓冲区 通常没有内核缓冲区 有内核缓冲区(页缓存)
设备节点 /dev/ttyS0, /dev/input/mouse0 /dev/sda, /dev/mmcblk0
主设备号范围 1-255(动态分配) 1-255(动态分配)
典型应用 串口、GPIO、I2C、SPI 硬盘、SSD、SD卡、U盘

说白了,字符设备就是"流式"的,你读一个字节它就给你一个字节。块设备是"块式"的,你读数据必须按块来,比如512字节或4KB。我曾经调试过一个USB转串口驱动,就是因为没搞清楚这个区别,在应用层用块设备的方式去读字符设备,结果数据全乱了。嗯,这个坑我替你们踩过了。

避坑指南:我曾经在项目里遇到一个情况,同事把字符设备驱动注册成了块设备类型,结果系统启动时直接panic。记住:字符设备用c表示,块设备用b表示,在mknod创建节点时千万别搞混了。

1.3 字符设备驱动框架介绍

好了,现在咱们进入正题。字符设备驱动的框架,说白了就是一套固定的"套路"。你只要按照这个套路来,就能写出一个能用的驱动。

先看一张我画的框架图,这样更直观:

字符设备驱动框架图 应用层(用户空间) 应用程序:open() / read() / write() / ioctl() / close() 系统调用 虚拟文件系统(VFS) file_operations 结构体查找 驱动层(内核空间) file_operations 回调函数实现 open → xxx_open | read → xxx_read | write → xxx_write | ioctl → xxx_ioctl 硬件层 寄存器操作 / 中断处理 / DMA传输 数据流向

这张图展示了字符设备驱动的完整调用链路。从应用层到硬件层,一共四层。你写驱动主要工作在第三层——驱动层。

一个标准的字符设备驱动,核心就是实现 file_operations 结构体。我给大家看一个最简示例:

#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>

static int my_open(struct inode *inode, struct file *filp)
{
    printk(KERN_INFO "设备已打开\n");
    return 0;
}

static ssize_t my_read(struct file *filp, char __user *buf, 
                       size_t count, loff_t *f_pos)
{
    printk(KERN_INFO "读取设备数据\n");
    return 0;
}

static ssize_t my_write(struct file *filp, const char __user *buf,
                        size_t count, loff_t *f_pos)
{
    printk(KERN_INFO "写入设备数据\n");
    return count;
}

static int my_release(struct inode *inode, struct file *filp)
{
    printk(KERN_INFO "设备已关闭\n");
    return 0;
}

static struct file_operations my_fops = {
    .owner   = THIS_MODULE,
    .open    = my_open,
    .read    = my_read,
    .write   = my_write,
    .release = my_release,
};

// 模块初始化与退出函数(简化版)
static int __init my_init(void)
{
    // 注册字符设备(具体实现后续章节详解)
    printk(KERN_INFO "字符设备驱动加载成功\n");
    return 0;
}

static void __exit my_exit(void)
{
    // 注销字符设备
    printk(KERN_INFO "字符设备驱动卸载成功\n");
}

module_init(my_init);
module_exit(my_exit);
MODULE_LICENSE("GPL");

经验之谈:我刚开始写驱动时,总是忘记在 file_operations 里设置 .owner 字段。这个字段虽然看起来不起眼,但它能防止模块在使用中被卸载。有一次我调试到半夜,就是因为这个字段没设,模块一卸载系统就崩了。所以,.owner = THIS_MODULE 一定要写上。

字符设备驱动的开发流程,我总结为以下几步:

  1. 分配设备号:静态分配或动态分配主次设备号
  2. 初始化cdev:调用 cdev_init() 绑定 file_operations
  3. 添加设备:调用 cdev_add() 将设备注册到系统
  4. 创建设备节点:手动 mknod 或通过 udev 自动创建
  5. 实现操作函数:编写 open/read/write/ioctl 等回调
  6. 清理退出:卸载时删除cdev、释放设备号

这个流程我建议你背下来。为什么?因为几乎所有字符设备驱动都逃不出这个框架。你以后写I2C驱动、SPI驱动、GPIO驱动,底层逻辑都是这套东西。

核心总结:字符设备驱动的本质就是:注册一个 file_operations 结构体,告诉内核"当用户访问这个设备文件时,调用我写的这些函数"。就这么简单。

好了,这一章的内容就到这里。记住我上面说的框架和流程,下一章咱们会深入讲解设备号的分配与管理。到时候我会分享一些我在实际项目中踩过的坑,保证让你少走弯路。


可可精品资料库