定义

Linux 的设备管理是和文件系统紧密结合的,各种设备都以文件的形式存放在/dev 目录下,称为设备文件。

  • 应用程序可以打开、关闭和读写这些设备文件,完成对设备的操作,就像操作普通的数据文件一样。
  • 系统为设备编了号,每个设备号又分为主设备号和次设备号。主设备号用来区分不同类型的设备,而次设备号用来区分同一类型的多个设备。

设备号的组成

一个字符设备或者块设备都有一个主设备号和次设备号。主设备号和次设备号统称为设备号。

  • 主设备号用来表示一个特定的驱动程序。次设备号用来表示使用该驱动程序的各个设备。
  • Linux 提供了一个名为dev_t 的数据类型表示设备号,在文件 include/linux/types.h 中定义:
typedef __u32 __kernel_dev_t;
typedef __kernel_dev_t dev_t 
  • dev_t 是个 32 位的变量,其中 12 位用来表示主设备号,20 位用来表示次设备号。因此 Linux 系统中主设备号范围0~4095
  • 操作设备号的宏定义:
#define MINORBITS 20 // 次设备号的位数,一共是20位
 
#define MINORMASK ((1U << MINORBITS) - 1) // 次设备号的掩码
 
#define MAJOR(dev) ((unsigned int)((dev) >> MINORBITS)) // 在dev_t 里面获取我们的主设备号
 
#define MINOR(dev) ((unsigned int)((dev) & MINORMASK)) // 在dev_t 里面获取我们的次设备号
 
#define MKDEV(ma, mi) (((ma) << MINORBITS) | (mi)) // 将我们的主设备号和次设备号组成一个dev_t类型。第一个参数是主设备号,第二个参数是次设备号

设备号的分配

静态设备号分配

驱动程序开发者通过静态指定一个设备号。

  • 对于一部分常用的设备,内核开发者已经为其分配了设备号。
  • 这些设备号可以在内核源码 documentation/ devices.txt 文件中找到。
  • 使用cat/proc/devices可以查看当前系统已经使用的主设备号
  • 设备号的静态申请函数:
#include <linux/fs.h>
 
/**
 * \brief 注册一个字符设备区域
 *
 * 此函数用于在内核中注册一个字符设备区域,分配一组连续的主次设备号,
 * 并将这些设备号与指定的名称关联起来。
 *
 * \param dev 指向设备号的指针,函数将分配的设备号存储在此处
 * \param count 要分配的设备号数量
 * \param name 设备的名称
 *
 * \return 如果成功,返回 0;否则返回一个负的错误代码
 *
 * \note
 * - 成功注册后,设备号将被分配给设备,并且可以在后续的操作中使用。
 * - 如果注册失败,通常是因为设备号已经被占用或者无法分配足够的设备号。
 */
int register_chrdev_region(dev_t *dev, unsigned count, const char *name);

动态设备号分配

静态设备号容易导致设备冲突

  • 系统会自动给你一个没有被使用的设备号,这样就避免了冲突。
  • 卸载驱动的时候释放掉这个设备号即可。
  • 设备号的动态申请函数:
#include <linux/fs.h>
 
/**
 * \brief 分配并注册一组连续的字符设备号
 *
 * 此函数用于在内核中分配一组连续的字符设备号,并将这些设备号与指定的名称关联起来。
 *
 * \param dev 指向设备号的指针,函数将分配的设备号存储在此处
 * \param baseminor 基础次设备号,从这个次设备号开始分配
 * \param count 要分配的设备号数量
 * \param name 设备的名称
 *
 * \return 如果成功,返回 0;否则返回一个负的错误代码
 *
 * \note
 * - 成功注册后,设备号将被分配给设备,并且可以在后续的操作中使用。
 * - 如果注册失败,通常是因为设备号已经被占用或者无法分配足够的设备号。
 */
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);

注销字符设备之后要释放掉设备号

  • 注销设备号:
#include <linux/fs.h>
 
/**
 * \brief 释放一组连续的字符设备号
 *
 * 此函数用于释放之前通过 `alloc_chrdev_region` 或其他类似函数分配的一组连续的字符设备号。
 *
 * \param from 起始设备号
 * \param count 要释放的设备号数量
 *
 * \note
 * - 释放设备号后,这些设备号可以被重新分配给其他设备。
 * - 在模块卸载时通常会调用此函数来释放设备号。
 */
void unregister_chrdev_region(dev_t from, unsigned count);

示例

编写驱动

  • 完整代码
/*
 * @Descripttion: 动态或者静态申请字符设备号
 * @version: 
 * @Author: topeet
 */
#include <linux/init.h>   //初始化头文件
#include <linux/module.h> //最基本的文件,支持动态添加和卸载模块。
#include <linux/fs.h>     //包含了文件操作相关struct的定义,例如大名鼎鼎的struct file_operations
#include <linux/kdev_t.h> //
 
#define DEVICE_NUMBER 1        //定义次设备号的个数
#define DEVICE_SNAME "schrdev" //定义静态注册设备的名称
#define DEVICE_ANAME "achrdev" //定义动态注册设备的名称
#define DEVICE_MINOR_NUMBER 0  //定义次设备号的起始地址
 
static int major_num, minor_num;       //定义主设备号和次设备号
module_param(major_num, int, S_IRUSR); //驱动模块传入普通参数major_num
module_param(minor_num, int, S_IRUSR); //驱动模块传入普通参数minor_num
 
static int hello_init(void)
{
    dev_t dev_num;
    int ret; //函数返回值
    if (major_num)
    {
        /*静态注册设备号*/
        printk("major_num = %d\n", major_num);                              //打印传入进来的主设备号
        printk("minor_num = %d\n", minor_num);                              //打印传入进来的次设备号
        dev_num = MKDEV(major_num, minor_num);                              //MKDEV将主设备号和次设备号合并为一个设备号
        ret = register_chrdev_region(dev_num, DEVICE_NUMBER, DEVICE_SNAME); //注册设备号
        if (ret < 0)
        {
            printk("register_chrdev_region error\n");
        }
        //静态注册设备号成功,则打印。
        printk("register_chrdev_region ok\n");
    }
    else
    {
        /*动态注册设备号*/
        ret = alloc_chrdev_region(&dev_num, DEVICE_MINOR_NUMBER, 1, DEVICE_ANAME);
        if (ret < 0)
        {
            printk("alloc_chrdev_region error\n");
        }
        //动态注册设备号成功,则打印
        printk("alloc_chrdev_region ok\n");
        major_num = MAJOR(dev_num);            //将主设备号取出来
        minor_num = MINOR(dev_num);            //将次设备号取出来
        printk("major_num = %d\n", major_num); //打印传入进来的主设备号
        printk("minor_num = %d\n", minor_num); //打印传入进来的次设备号
    }
    return 0;
}
 
static void hello_exit(void)
{
    unregister_chrdev_region(MKDEV(major_num, minor_num), DEVICE_NUMBER); //注销设备号
    //printk("a = %d \n",a);
    printk("gooodbye! \n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");

编译驱动

obj-m += chrdev.o
KDIR:=/home/topeet/linux/linux-imx
PWD?=$(shell pwd)
all:
	make -C $(KDIR) M=$(PWD) modules ARCH=arm64
clean:
	make -C $(KDIR) M=$(PWD) clean

静态设备号测试

  • 首先使用cat/proc/devices查看已使用的设备号
  • 设置一个主设备号9
insmod chrdev.ko major_num=9
  • 再次查看设备号可发现设备号9 schrdev
  • 使用remod chrdev卸载驱动后再次查看设备号发现设备号已消失

动态设备号测试

  • 使用insmod chrdev.ko动态注册方式加载设备号
  • 使用cat/proc/devices查看新生成的设备号
  • 使用remod chrdev卸载驱动后再次查看设备号发现设备号已消失