【Linux驱动开发 ---- 3_设备驱动基本概念】

Linux驱动开发 ---- 3_设备驱动基本概念

🎯 目标:

理解 Linux 中设备驱动的基本分类、作用与结构,掌握主设备号、次设备号、设备文件之间的联系。


📌 1. 什么是设备驱动?

设备驱动是操作系统内核中的一段代码,它充当操作系统与硬件设备之间的桥梁。用户空间无法直接访问硬件,必须通过驱动进行间接访问。


📌 2. Linux 中的设备分类

Linux 将设备分为以下几类:

类型描述示例
字符设备数据以字符流方式传输串口、键盘、鼠标、LED
块设备数据以固定块大小读写硬盘、U盘、SD 卡
网络设备用于网络通信的数据收发网卡、Wi-Fi 模块

📌 3. 主设备号 & 次设备号

Linux 使用 设备号(device number) 来标识设备。设备号由两个部分组成:

  • 主设备号(Major Number):标识使用哪个驱动程序来操作设备。
  • 次设备号(Minor Number):标识同一个驱动管理下的不同设备。

设备号用 dev_t 类型表示(32 位):

MAJOR(dev_t dev);  // 获取主设备号
MINOR(dev_t dev);  // 获取次设备号
MKDEV(int major, int minor); // 创建一个 dev_t 类型的设备号

📌 4. /dev 目录中的设备文件

  • Linux 中所有设备都以“文件”的形式出现在 /dev 下。
  • 用户通过访问这些 设备文件,内核通过背后的驱动程序来控制设备。

👀 查看设备文件:

ls -l /dev | grep tty

输出示例:

crw-rw---- 1 root dialout 4, 0 Apr 6 09:12 tty0
  • c 表示字符设备,b 表示块设备。
  • 4, 0 就是主设备号 4,次设备号 0
    在这里插入图片描述

📌 5. 如何查看设备号和驱动关系?

cat /proc/devices

输出示例:

Character devices:
  1 mem
  4 tty
  5 /dev/tty
  ...

这意味着主设备号为 4 的字符设备是 tty 驱动管理的。


📌 6. 实践任务:设备文件和设备号探索

✅ 步骤一:查看已有设备号和对应驱动

cat /proc/devices

在这里插入图片描述

✅ 步骤二:查找一个字符设备的主/次设备号

ls -l /dev/null

结果:

crw-rw-rw- 1 root root 1, 3 /dev/null

在这里插入图片描述

说明 /dev/null 是字符设备,主设备号 1,次设备号 3。


📌 7. 实践代码:模拟注册一个字符设备(不含实际操作)

🧪 创建一个简单字符设备框架(demo_chrdev.c)

#include <linux/init.h>         // 模块初始化/退出宏:module_init, module_exit
#include <linux/module.h>       // 基本模块宏,如 MODULE_LICENSE
#include <linux/fs.h>           // 分配/注册设备号,file_operations 等
#include <linux/cdev.h>         // cdev 结构体及相关函数
#include <linux/device.h>       // class_create, device_create 等
#include <linux/uaccess.h>      // copy_to_user, copy_from_user
#include <linux/string.h>       // strlen

#define DEVICE_NAME "demo_chrdev"

static int major;                       // 主设备号
static struct class *cls;              // 设备类,用于在 /sys/class 下创建设备节点
static struct cdev demo_cdev;          // 字符设备结构体

// 打开设备函数
static int demo_open(struct inode *inode, struct file *file) {
    printk(KERN_INFO "demo_chrdev: device opened\n");
    return 0;
}

// 读取设备函数
static ssize_t demo_read(struct file *file, char __user *buf, size_t len, loff_t *offset) {
    const char *msg = "Hello from kernel!\n";
    size_t msg_len = strlen(msg);

    // 如果偏移量超过数据长度,说明已经读完
    if (*offset >= msg_len)
        return 0;

    // 若请求读取长度超过剩余长度,进行裁剪
    if (len > msg_len - *offset)
        len = msg_len - *offset;

    // 从内核空间复制数据到用户空间
    if (copy_to_user(buf, msg + *offset, len))
        return -EFAULT;

    *offset += len;  // 更新偏移
    return len;      // 返回实际读取的字节数
}

// 文件操作集
static const struct file_operations demo_fops = {
    .owner = THIS_MODULE,
    .open = demo_open,
    .read = demo_read,
};

// 模块初始化函数
static int __init demo_init(void) {
    dev_t dev;
    int ret;

    // 分配一个设备号(动态分配主设备号)
    ret = alloc_chrdev_region(&dev, 0, 1, DEVICE_NAME);
    if (ret < 0) {
        printk(KERN_ERR "Failed to alloc major number\n");
        return ret;
    }

    major = MAJOR(dev);  // 获取主设备号
    printk(KERN_INFO "demo_chrdev: registered with major %d\n", major);

    // 初始化 cdev 结构并注册到内核
    cdev_init(&demo_cdev, &demo_fops);
    demo_cdev.owner = THIS_MODULE;
    cdev_add(&demo_cdev, dev, 1);

    // 创建类和设备(这样在 /dev 目录下会自动生成对应设备节点)
    cls = class_create(THIS_MODULE, DEVICE_NAME);
    device_create(cls, NULL, dev, NULL, DEVICE_NAME);

    return 0;
}

// 模块退出函数
static void __exit demo_exit(void) {
    dev_t dev = MKDEV(major, 0);

    device_destroy(cls, dev);         // 删除设备节点
    class_destroy(cls);               // 删除类
    cdev_del(&demo_cdev);             // 注销 cdev
    unregister_chrdev_region(dev, 1); // 释放设备号

    printk(KERN_INFO "demo_chrdev: unregistered\n");
}

// 注册模块初始化和退出函数
module_init(demo_init);
module_exit(demo_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("YourName");
MODULE_DESCRIPTION("Simple Character Device Example");

🧪 实操步骤

1). 创建驱动文件

vim demo_chrdev.c

将上面的代码粘贴进去。


2). 创建 Makefile

obj-m := demo_chrdev.o

KDIR := /lib/modules/$(shell uname -r)/build
PWD  := $(shell pwd)

all:
	make -C $(KDIR) M=$(PWD) modules

clean:
	make -C $(KDIR) M=$(PWD) clean

在这里插入图片描述


3). 编译模块

make

成功后会生成 demo_chrdev.ko 模块文件。


在这里插入图片描述

4). 加载模块

sudo insmod demo_chrdev.ko

查看 dmesg 输出:

dmesg | tail

你会看到如:

demo_chrdev: registered with major 240

在这里插入图片描述


5). 查看并测试设备节点

ls /dev/demo_chrdev

在这里插入图片描述

如果存在,表示 device_create 已成功创建了字符设备节点。

读取字符设备的内容:

cat /dev/demo_chrdev

应该输出:

Hello from kernel!

在这里插入图片描述


6). 卸载模块

sudo rmmod demo_chrdev

再次查看 dmesg

dmesg | tail

会显示:

demo_chrdev: unregistered

在这里插入图片描述


📌 8. 总结回顾

内容项说明
字符/块/网络设备不同类型的设备按数据交互方式分类
主设备号标识一个驱动
次设备号区分同类设备
/dev 设备文件用户空间访问设备的接口
/proc/devices显示当前系统已注册的主设备号与驱动对应关系
file_operations驱动操作函数集合
cdev 注册字符设备在内核中注册实际设备的关键步骤

🧠 今日练习题(可选)

  1. 解释主设备号和次设备号的作用。

    🔧 什么是主设备号(major number)和次设备号(minor number)?

在 Linux 中,每个设备文件(例如 /dev/demo_chrdev)都通过一个“设备号”来和内核中的驱动程序建立联系。

这个设备号是一个 32 位整数,由两部分组成

部分名称作用
高 12 位主设备号(major)用于标识驱动程序
低 20 位次设备号(minor)用于标识具体的设备实例

例如你创建的 /dev/demo_chrdev 可能对应的是:

设备号 dev_t = 240:0   // 主设备号240,次设备号0

🔹 主设备号(Major Number)

主设备号的作用是:🔗 将设备文件和具体的驱动程序绑定起来。

当你操作设备文件 /dev/demo_chrdev 时,Linux 内核会:

  1. 查找该文件的 主设备号
  2. 根据主设备号找到对应的 file_operations 结构(也就是你的驱动程序);
  3. 调用 .open().read().write() 等函数。

举个例子:

  • /dev/sda/dev/sdb 可能都由同一个驱动程序(比如 SCSI 磁盘驱动)管理,那么它们主设备号是一样的(比如 8)。
  • 内核知道:主设备号 8 → scsi_disk_driver

🔸 次设备号(Minor Number)

次设备号的作用是:🔍 在同一个驱动程序中区分不同的设备实例

一个驱动可能控制多个设备。例如:

设备文件主设备号次设备号说明
/dev/led02400第 0 个 LED
/dev/led12401第 1 个 LED
/dev/led22402第 2 个 LED

虽然都是由主设备号 240 控制,但你在驱动里的 .open() 函数可以通过 iminor(inode) 获得当前打开的是哪个设备。


✅ 如何在驱动中获取主次设备号?

在 open 函数中可以这么写:

static int demo_open(struct inode *inode, struct file *file)
{
    int major = imajor(inode);
    int minor = iminor(inode);
    printk(KERN_INFO "demo_chrdev: open - major=%d, minor=%d\n", major, minor);
    return 0;
}

🧪 补充知识:动态分配 vs 静态指定主设备号

✅ 动态分配(推荐):

alloc_chrdev_region(&dev, 0, 1, DEVICE_NAME);

由内核自动分配主设备号,避免冲突,适合模块式开发。

❌ 静态指定(可能冲突):

register_chrdev_region(MKDEV(240, 0), 1, DEVICE_NAME);

你自己指定主设备号,需确保没被系统占用,否则会失败。


🎯 总结

项目主设备号(Major)次设备号(Minor)
作用识别使用哪个驱动识别使用驱动中的哪个设备
驱动层处理用于注册 file_operations用于驱动内部区分设备实例
数量一般一个驱动对应一个主号一个主号下可以有多个次号

  1. ls -l 命令找出 /dev/zero 的主设备号与次设备号。
    在这里插入图片描述

  2. 写出一个 file_operations 结构体中最常用的 3 个函数名称及作用。

    🔧 最常用的 file_operations 中的 3 个函数如下:

函数名作用说明
.open当用户通过 open() 打开设备文件时调用。通常用于设备初始化、分配资源等。
.read当用户通过 read() 从设备中读取数据时调用。用于从设备读取数据并传递给用户空间。
.write当用户通过 write() 向设备写入数据时调用。用于接收用户空间数据并传入内核或设备处理。

👇 简单示意:

static const struct file_operations my_fops = {
    .owner = THIS_MODULE,     // 指定模块所有者,防止模块被卸载
    .open  = my_open,         // 设备打开
    .read  = my_read,         // 读取数据
    .write = my_write,        // 写入数据
};

🔍 各函数的原型:

int open(struct inode *inode, struct file *file);
ssize_t read(struct file *file, char __user *buf, size_t count, loff_t *pos);
ssize_t write(struct file *file, const char __user *buf, size_t count, loff_t *pos);


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值