Skip to content

LV080-兼容多个设备

可不可以通过一个驱动控制多个设备?怎么实现?若笔记中有错误或者不合适的地方,欢迎批评指正 😃。

一、概述

1. 背景

在 Linux 内核中,主设备号用于标识设备对应的驱动程序,告诉 Linux 内核使用哪一个驱动程序为该设备服务。但是, 次设备号表示了同类设备的各个设备。每个设备的功能都是不一样的。如何能够用一个驱动程序去控制各种设备呢?

很明显,首先,我们可以根据次设备号,来区分各种设备;其次,就是提到过的 file 结构体的私有数据成员 private_data。 我们可以通过该成员来指向不同的设备,不难想到为什么只有 open 函数和 close 函数的形参才有 file 结构体, 因为驱动程序第一个执行的是操作就是 open,通过 open 函数就可以控制我们想要驱动的底层硬件。

2. 可以有哪些方式

有两种方式:

  • (1)一个 cdev 对象管理多个设备;
  • (2)多个 cdev 对象,每个 cdev 对象管理一个设备。

3. inode 结构体

前面学习这个 inode 结构体的时候有这么两个成员:fs.h - include/linux/fs.h - struct inode

c
struct inode {
	// ......
	dev_t			i_rdev;
	// ......
	union {
		struct pipe_inode_info	*i_pipe;
		struct block_device	*i_bdev;
		struct cdev		*i_cdev;
		char			*i_link;
		unsigned		i_dir_seq;
	};
	// ......
} __randomize_layout;
  • dev_t i_rdev: 表示设备文件的结点,这个域实际上包含了设备号。
  • struct cdev *i_cdev: struct cdev 是内核的一个内部结构,它是用来表示字符设备的,当 inode 结点指向一个字符设备文件时,此域为一个指向 inode 结构的指针。

一个含有设备号,一个指向字符设备,我们可以利用这两个成员来区分不同设备的。后面会说明这两个成员怎么用。

二、1 个 cdev 设备

1. 怎么实现?

1 个 cdev 的话,我们只需要一个 cdev 对象,一个类,但是设备号需要创建多个,并且根据不同的次设备号创建多个设备节点,用来控制不同的缓冲区。

image-20241226105838093

不同的设备节点对应不同的次设备号,我们可以在 open 的时候根据设备节点,找到设备号,根据次设备号来对应不同的缓冲区。那另一个问题来了,怎么在 open 的时候获取次设备号?

inode 结构体中,对于设备文件的设备号会被保存到其成员 i_rdev 中,所以:

c
static int sdev_open(struct inode *inode, struct file *file)
{
    
    switch (MINOR(inode->i_rdev))
    {
        case 0:
        {
            file->private_data = &g_sdev_child[0]; //设置私有数据
            break;
        }
        case 1:
        { 
            file->private_data = &g_sdev_child[1]; //设置私有数据
            break;
        }
        default:
        {
            file->private_data = &g_sdev_child[0]; //设置私有数据
            break;
        }
    }
    printk("This is sdev_open!inode->i_rdev=%d, minor=%d\n", inode->i_rdev, MINOR(inode->i_rdev));
    return 0;
}

为什么?这个我没有去深究 inode-> i_rdev 中怎么吧设备号保存下来的,我查到在使用 mknod 命令创建节点的时候会调用这个函数 inode.c - fs/inode.c - init_special_inode

c
void init_special_inode(struct inode *inode, umode_t mode, dev_t rdev)
{
	inode->i_mode = mode;
	if (S_ISCHR(mode)) {
		inode->i_fop = &def_chr_fops;
		inode->i_rdev = rdev;
	} 
 //......
}
EXPORT_SYMBOL(init_special_inode);

会发现这里是有赋值的,大概应该是这里,不过自动创建节点的函数中怎么赋值的我追了一下代码,没找到,这里先不管了,知道它保存的是设备号就是了,后面有需要了再详细去了解。

2. demo 实现

直接看我的 gitee 仓库吧:04_chrdev_basic/09_chrdev_more_dev_1c

image-20260119103549189

直接执行 make 即可进行编译,编译完成后会得到对应的驱动程序和应用测试程序。

c
#include <linux/init.h> /* module_init module_exit */
#include <linux/kernel.h>
#include <linux/module.h> /* MODULE_LICENSE */

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

#include "./timestamp_autogenerated.h"
#include "./version_autogenerated.h"

#define CHRDEV_NAME "sdev"    /* 设备名, cat /proc/devices 查看与设备号的对应关系 */
#define CLASS_NAME  "sclass"  /* 类名,在 /sys/class 中显示的名称 */
#define DEVICE_NAME "sdevice" /* 设备节点名,在 /sys/class/class_name/ 中显示的名称以及 /dev/ 下显示的节点名 */
#define BUFSIZE     32        /* 设置最大偏移量为 32 */

#define CHRDEV_CNT (2)        /* 设备数量 */

#define CMD_TEST0 _IO('S', 0)
#define CMD_TEST1 _IOW('S', 1, int)
#define CMD_TEST2 _IOR('S', 2, int)
#define CMD_TEST3 _IOW('S', 3, int)

struct __CMD_TEST{
	int a;
	int b;
	int c;
};

typedef struct __CHAR_DEVICE
{
    dev_t dev_num;         // 定义 dev_t 类型(32 位大小)的变量 dev_num, 用来存放设备号
    struct cdev s_cdev;    // 定义 cdev 结构体类型的变量 scdev
    struct class *class;   // 定于 struct class *类型结构体变量 class,表示要创建的类
} _CHAR_DEVICE;

typedef struct __CHAR_DEVICE_CHILD
{
    struct device *device; // 设备
    char buf[BUFSIZE];     // 设置数据存储数组 mem
}_CHAR_DEVICE_CHILD;

_CHAR_DEVICE g_sdev;  //定义一个 g_sdev 结构体变量,只包含一个 cdev 对象
_CHAR_DEVICE_CHILD g_sdev_child[CHRDEV_CNT] = {0}; // 一个 cdev 对象要管理的 CHRDEV_CNT 个设备


static int sdev_open(struct inode *inode, struct file *file)
{
    
    switch (MINOR(inode->i_rdev))
    {
        case 0:
        {
            file->private_data = &g_sdev_child[0]; //设置私有数据
            break;
        }
        case 1:
        { 
            file->private_data = &g_sdev_child[1]; //设置私有数据
            break;
        }
        default:
        {
            file->private_data = &g_sdev_child[0]; //设置私有数据
            break;
        }
    }
    printk("This is sdev_open!inode->i_rdev=%d, minor=%d\n", inode->i_rdev, MINOR(inode->i_rdev));
    return 0;
}

static ssize_t sdev_read(struct file *file, char __user *buf, size_t size, loff_t *off)
{
    int i = 0;
    loff_t offset = *off; // 将读取数据的偏移量赋值给 loff_t 类型变量 p
    size_t count = size;
    _CHAR_DEVICE_CHILD *pDev=(_CHAR_DEVICE_CHILD *)file->private_data; //在 write 函数中读取 private_data

    if (offset > BUFSIZE)
    {
        return 0;
    }

    if (count > BUFSIZE - offset)
    {
        count = BUFSIZE - offset;
    }

    if (copy_to_user(buf, pDev->buf + offset, count))
    { 
        // 将 mem 中的值写入 buf,并传递到用户空间
        printk("copy_to_user error!\n");
        return -1;
    }

    for (i = 0; i < BUFSIZE; i++)
    {
        printk("buf[%d] %c\n", i, pDev->buf[i]); // 将 mem 中的值打印出来
    }
    printk("read offset is %llu, count is %d\n", offset, count);
    *off = *off + count; // 更新偏移值

    return count;
}

static ssize_t sdev_write(struct file *file, const char __user *buf, size_t size, loff_t *off)
{
    int i = 0;
    loff_t offset = *off; // 将读取数据的偏移量赋值给 loff_t 类型变量 p
    size_t count = size;
    _CHAR_DEVICE_CHILD *pDev=(_CHAR_DEVICE_CHILD *)file->private_data; //在 write 函数中读取 private_data

    if (offset > BUFSIZE)
    {
        return 0;
    }

    if (count > BUFSIZE - offset)
    {
        count = BUFSIZE - offset;
    }

    if (copy_from_user(pDev->buf + offset, buf, count))
    { 
        // 将 buf 中的值,从用户空间传递到内核空间
        printk("copy_to_user error \n");
        return -1;
    }

    for (i = 0; i < BUFSIZE; i++)
    {
        printk("buf[%d] %c\n", i, pDev->buf[i]); // 将 mem 中的值打印出来
    }
    printk("write offset is %llu, count is %d\n", offset, count); // 打印写入的值
    *off = *off + count;                         // 更新偏移值

    return count;
}

static int sdev_release(struct inode *inode, struct file *file)
{
    printk("This is sdev_release!\n");
    return 0;
}

static loff_t sdev_llseek(struct file *file, loff_t offset, int whence)
{
    loff_t new_offset = 0; // 定义 loff_t 类型的新的偏移值
    switch (whence)    // 对 lseek 函数传递的 whence 参数进行判断
    {
        case SEEK_SET:
            if (offset < 0 || offset > BUFSIZE)
            {
                return -EINVAL; // EINVAL = 22 表示无效参数
            }
            new_offset = offset; // 如果 whence 参数为 SEEK_SET,则新偏移值为 offset
            break;
        case SEEK_CUR:
            if ((file->f_pos + offset < 0) || (file->f_pos + offset > BUFSIZE))
            {
                return -EINVAL;
            }
            new_offset = file->f_pos + offset; // 如果 whence 参数为 SEEK_CUR,则新偏移值为 file-> f_pos + offset,file-> f_pos 为当前的偏移值
            break;
        case SEEK_END:
            if (file->f_pos + offset < 0)
            {
                return -EINVAL;
            }
            new_offset = BUFSIZE + offset; // 如果 whence 参数为 SEEK_END,则新偏移值为 BUFSIZE + offset,BUFSIZE 为最大偏移量
            break;
        default:
            break;
    }
    file->f_pos = new_offset; // 更新 file-> f_pos 偏移值

    return new_offset;
}

static long sdev_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
    int val = 0;//定义 int 类型向应用空间传递的变量 val
    switch(cmd)
    {
        case CMD_TEST0:
            printk("this is CMD_TEST0\n");
            break;
        case CMD_TEST1:
            printk("this is CMD_TEST1\n");
            printk("arg is %ld\n",arg);//打印应用空间传递来的 arg 参数
            break;
        case CMD_TEST2:
            val = 1;//将要传递的变量 val 赋值为 1
            printk("this is CMD_TEST2\n");
            if(copy_to_user((int *)arg, &val, sizeof(val)) != 0)
            {
                //通过 copy_to_user 向用户空间传递数据
                printk("copy_to_user error \n");
            }
            break;
        case CMD_TEST3:
        {
            struct __CMD_TEST cmd_test3 = {0};
            if (copy_from_user(&cmd_test3, (int *)arg, sizeof(cmd_test3)) != 0)
            {
                printk("copy_from_user error\n");
            }
            printk("cmd_test3.a = %d\n", cmd_test3.a);
            printk("cmd_test3.b = %d\n", cmd_test3.b);
            printk("cmd_test3.c = %d\n", cmd_test3.c);
            break;
        }
        default:
            break;
    }

    return 0;
}

static struct file_operations cdev_ops = {
    .owner = THIS_MODULE,//将 owner 字段指向本模块,可以避免在模块的操作正在被使用时卸载该模块
	.open = sdev_open,
	.read = sdev_read,
	.write = sdev_write,
	.release = sdev_release,
    .llseek = sdev_llseek,
    .unlocked_ioctl = sdev_ioctl,
}; // 定义 file_operations 结构体类型的变量 g_cdev_dev_ops

// 模块入口函数
static int __init chrdev_more_dev_1c_demo_init(void)
{
    int i = 0;
    int ret = 0;          // 定义 int 类型的变量 ret,用来判断函数返回值
    int major = 0, minor = 0; // 定义 int 类型的主设备号 major 和次设备号 minor
    printk("*** [%s:%d]Build Time: %s %s, git version:%s ***\n", __FUNCTION__,
           __LINE__, KERNEL_KO_DATE, KERNEL_KO_TIME, KERNEL_KO_VERSION);
    printk("chrdev_more_dev_1c_demo module init!\n");

    ret = alloc_chrdev_region(&g_sdev.dev_num, 0, CHRDEV_CNT, CHRDEV_NAME); // 自动获取设备号,设备名为 chrdev_name
    if (ret < 0)
    {
        printk("alloc_chrdev_region is error!\n");
    }
    printk("alloc_register_region is ok!\n");
    major = MAJOR(g_sdev.dev_num); // 使用 MAJOR()函数获取主设备号
    minor = MINOR(g_sdev.dev_num); // 使用 MINOR()函数获取次设备号
    printk("major is %d, minor is %d !\n", major, minor);

    cdev_init(&g_sdev.s_cdev, &cdev_ops); // 使用 cdev_init()函数初始化 g_sdev.s_cdev 结构体,并链接到 cdev_ops 结构体
    g_sdev.s_cdev.owner = THIS_MODULE;          // 将 owner 字段指向本模块,可以避免在模块的操作正在被使用时卸载该模块
    ret = cdev_add(&g_sdev.s_cdev, g_sdev.dev_num, CHRDEV_CNT); // 使用 cdev_add()函数进行字符设备的添加
    if (ret < 0)
    {
        printk("cdev_add is error !\n");
    }
    printk("cdev_add is ok!CHRDEV_CNT=%d\n", CHRDEV_CNT);

    g_sdev.class = class_create(THIS_MODULE, CLASS_NAME);          // 使用 class_create 进行类的创建,类名称为 class_dev
    
    // 这里还是需要创建多个设备的
    for(i = 0; i < CHRDEV_CNT; i++)
    {
        g_sdev_child[i].device = device_create(g_sdev.class, NULL, g_sdev.dev_num + i, NULL, "%s%d",DEVICE_NAME, i); // 使用 device_create 进行设备的创建,设备名称为 device_dev
    }
    return 0;
}

// 模块出口函数
static void __exit chrdev_more_dev_1c_demo_exit(void)
{
    int i = 0;
    // 需要注意的是, 字符设备的注册要放在申请字符设备号之后,
    // 字符设备的删除要放在释放字符驱动设备号之前。
    cdev_del(&g_sdev.s_cdev);                // 使用 cdev_del()函数进行字符设备的删除
    unregister_chrdev_region(g_sdev.dev_num, CHRDEV_CNT); // 释放字符驱动设备号
    for(i = 0; i < CHRDEV_CNT; i++)
    {
        device_destroy(g_sdev.class, g_sdev.dev_num + i); // 删除创建的设备
    }
    class_destroy(g_sdev.class);           // 删除创建的类
    printk("chrdev_more_dev_1c_demo exit!\n");
}

module_init(chrdev_more_dev_1c_demo_init); // 将__init 定义的函数指定为驱动的入口函数
module_exit(chrdev_more_dev_1c_demo_exit); // 将__exit 定义的函数指定为驱动的出口函数

/* 模块信息(通过 modinfo chrdev_more_dev_1c_demo 查看) */
MODULE_LICENSE("GPL v2");            /* 源码的许可证协议 */
MODULE_AUTHOR("sumu");               /* 字符串常量内容为模块作者说明 */
MODULE_DESCRIPTION("Description");   /* 字符串常量内容为模块功能说明 */
MODULE_ALIAS("module's other name"); /* 字符串常量内容为模块别名 */

3. 开发板测试

  • (1)加载驱动
shell
insmod chrdev_more_dev_1c_demo.ko
  • (2)查看设备节点
shell
ls /dev/sdevice* -alh

会看到有如下打印信息:

shell
crw-rw----    1 0        root      246,   0 Jan  1 00:00 /dev/sdevice0
crw-rw----    1 0        root      246,   1 Jan  1 00:00 /dev/sdevice1

在创建设备节点的时候命名就是/dev/sdeviceN,所以这里是 0 和 1 两个设备节点,他们会对应不同的缓冲区。

  • (3)向设备节点写入数据
shell
echo 123456789 > /dev/sdevice0
echo 987654321 > /dev/sdevice1

# 或者使用应用测试程序
./xxx_demo_app.out /dev/sdevice 1 x # 从 x 位置读取
./xxx_demo_app.out /dev/sdevice 2 x # 从 x 位置写入
./xxx_demo_app.out /dev/sdevice 3 x # x 为任意值
  • (4)从设备节点读取数据
shell
cat /dev/sdevice0
cat /dev/sdevice1

# 或者使用应用测试程序
./xxx_demo_app.out /dev/sdevice 1 x # 从 x 位置读取
./xxx_demo_app.out /dev/sdevice 2 x # 从 x 位置写入
./xxx_demo_app.out /dev/sdevice 3 x # x 为任意值

三、多个 cdev 设备

1. 怎么实现?

我们现在有多个设备,他们的主设备号一样,次设备号不同,他们也可以使用不同的 cdev 设备对象,都需要进行注册。但是他们都属于同一个类,所以我们只需要创建一个类就可以了。

  • 1、设备号申请的时候要直接连续申请多个;
  • 2、向内核注册新字符设备的时候,每个设备都要注册;
  • 3、创建类的时候只需要创建一个;
  • 4、创建设备的时候需要创建多个设备;
  • 5、删除设备的时候需要删除多个;
  • 6、删除字符设备对象的时候需要删除多个。
image-20241226151337208

1.1 设备号申请与注销

设备号身申请的时候不需要申请多次,只需要申请一次,并且指定要申请的设备数量即可,以 alloc_chrdev_region 为例:char_dev.c - fs/char_dev.c - alloc_chrdev_region

c
alloc_chrdev_region(&parentDevno, 0, dev_cnt, NEWCHAR_DEV_NAME); /* 申请设备号 */

这样我们就可以得到一个“父”设备号 devno,这个设备号包含主设备号和次设备号:

c
major = MAJOR(parentDevno); /* 获取分配号的主设备号 */
minor = MINOR(parentDevno); /* 获取分配号的次设备号 */

我们可以从这个设备号中获取到 dev_cnt 个“子”设备号,”子“设备号怎么获取呢?这样来获取:

c
for(int i = 0; i < dev_cnt; i++)
{
    devno[i] = MKDEV(major, minor + i);
}
...
// 或者
for(int i = 0; i < dev_cnt; i++)
{
    devno[i] = parentDevno + i;
}
...

注销设备号的时候,直接注销多个设备即可:char_dev.c - fs/char_dev.c - unregister_chrdev_region

c
/* 注销设备号(将会删除 /proc/devices 中的设备号和对应的名称记录) */
unregister_chrdev_region(devno, dev_cnt);

1.2 设备注册与注销

设备注册主要是 cdev 对象的初始化和注册,这个是每个”子”设备都需要进行的:

c
struct cdev dev[dev_cnt]; /* 定义一个字符设备对象设备 */
for(int i = 0; i < dev_cnt; i++)
{
    dev[i].owner = THIS_MODULE;
    /* 初始化 cdev 对象, 给该对象添加操作函数集 */
    cdev_init(&dev[i], &func_ops);
    /* 向 Linux 系统添加字符设备 (cdev 结构体变量, 将会被添加到一个 hash 链表中) */
    /* 会在 /proc/devices 中创建设备号和对应的名称记录(最终只会有一条主设备号的记录,但还是要分多次添加) */
    cdev_add(&dev2, devno + i, 1); /* 此处是固定的一个一个添加 */
}

注销时候就是一个一个删除就行了:

c
/* 3.删除字符设备对象 cdev */
for (i = 0; i < dev_cnt; i++)
{
    cdev_del(&dev[i]);
}

1.3 类的创建与销毁

这些多个设备属于同一个类,我们只需要创建一个类就可以了:

c
/* 创建类(要注意无论是一个设备还是多个设备,类只有一个) */
p_class = class_create(THIS_MODULE, NEWCHAR_DEV_NAME);

删除时候也只需要删除一个类:

c
/* 删除类(只有一个类,删除一次即可) */
class_destroy(p_class);

1.4 设备创建与销毁

因为我们是多个设备,所以每个设备都需要进行创建

c
/* 创建设备(有几个设备,就创建几个) */
for (i = 0; i < dev_cnt; i++)
{
    /* 创建的设备为 /dev/dev_name */
    device[i] = device_create(p_class, NULL, devno[i], NULL, "%s%d", NEWCHAR_DEV_NAME, i);}
}

销毁的时候也要逐个销毁:

c
 /* 删除设备(多个设备,逐个进行删除) */
for (i = 0; i < dev_cnt; i++)
{
    /* 删除的是 /dev/dev_name */
    device_destroy(p_class, devno[i]);
}

1.5 怎么区分不同的设备?

还是一样的问题,之前我们是通过 inode→ i_rdev,也就是设备号来获取子设备号,然后找到对应的设备的缓冲区,理论上来说,多个 cdev 设备的时候,也可以通过这种方式,但是我没试。这次我们来使用 inode 的 i_cdev 成员。

inode 中的 i_cdev 成员保存了对应字符设备结构体的地址,但是我们的虚拟设备是把 cdev 封装起来的一个结构体,例如:

c
/* 新字符设备的属性 */
struct _my_dev
{
    /* 1.设备号与设备数量 */
    int major;   /* 主设备号:占高 12 位,用来表示驱动程序相同的一类设备,Linux 系统中主设备号范围为 0~4095 */
    int minor;   /* 次设备号:占低 20 位,用来表示被操作的哪个具体设备 */
    dev_t devno; /* 新字符设备的设备号:由主设备号和次设备号组成 */
    /* 2.字符设备对象 */
    struct cdev mydev; /* 定义一个字符设备对象设备 */
    /* 3.设备读写相关成员变量 */
    char mydev_buf[BUF_LEN]; /* 读写缓冲区 */
    int curlen;              /* 读写缓冲区中当前数据字节数 */

    /* 自动创建设备节点相关成员变量 */
    struct device *device;   /* 设备 */
};
struct _my_dev g_my_dev[NEWCHAR_DEV_CNT]; /* 定义新字符设备数组 */

那我们要如何能够得到虚拟设备的数据缓冲区呢?为此,Linux 提供了一个宏定义 container_of(),该宏可以根据结构体的某个成员的地址, 来得到该结构体的地址。

c
/**
 * container_of - cast a member of a structure out to the containing structure
 * @ptr:	the pointer to the member.
 * @type:	the type of the container struct this is embedded in.
 * @member:	the name of the member within the struct.
 *
 */
#define container_of(ptr, type, member) ({				\
	void *__mptr = (void *)(ptr);					\
	BUILD_BUG_ON_MSG(!__same_type(*(ptr), ((type *)0)->member) &&	\
			 !__same_type(*(ptr), void),			\
			 "pointer type mismatch in container_of()");	\
	((type *)(__mptr - offsetof(type, member))); })

该宏需要三个参数,分别是代表结构体成员的真实地址,结构体的类型以及结构体成员的名字。这个在这里就详细再去分析原理了,可以看后面参考资料的【1】和【2】。在 sdev_open()函数中,我们需要通过 inode 的 i_cdev 成员,来得到对应的虚拟设备结构体,并保存到文件指针 file 的私有数据成员中。 假如,我们打开虚拟设备 1,那么 inode-> i_cdev 便指向了 sdevice0 的成员 mydev,利用 container_of 宏, 我们就可以得到 g_my_dev 结构体的地址,也就可以操作对应的数据缓冲区了。

2. demo 实现

直接看 gitee 仓库吧:04_chrdev_basic/10_chrdev_more_dev_mc

c
#include <linux/init.h> /* module_init module_exit */
#include <linux/kernel.h>
#include <linux/module.h> /* MODULE_LICENSE */

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

#include "./timestamp_autogenerated.h"
#include "./version_autogenerated.h"

#define CHRDEV_NAME "sdev"    /* 设备名, cat /proc/devices 查看与设备号的对应关系 */
#define CLASS_NAME  "sclass"  /* 类名,在 /sys/class 中显示的名称 */
#define DEVICE_NAME "sdevice" /* 设备节点名,在 /sys/class/class_name/ 中显示的名称以及 /dev/ 下显示的节点名 */
#define BUFSIZE     32        /* 设置最大偏移量为 32 */

#define CHRDEV_CNT (2)        /* 设备数量 */

#define CMD_TEST0 _IO('S', 0)
#define CMD_TEST1 _IOW('S', 1, int)
#define CMD_TEST2 _IOR('S', 2, int)
#define CMD_TEST3 _IOW('S', 3, int)

struct __CMD_TEST{
	int a;
	int b;
	int c;
};

/** 父设备号相关结构体,为了创建子设备号(子设备 0 的设备号与父设备号相同) 
 *  父设备可以看做是代表了整个驱动程序,父设备下挂 3 个具体的设备
 */
typedef struct __MY_DEV_PARENT
{
    /* 1.设备号与设备数量 */
    int major;   /* 主设备号:占高 12 位,用来表示驱动程序相同的一类设备,Linux 系统中主设备号范围为 0~4095 */
    int minor;   /* 次设备号:占低 20 位,用来表示被操作的哪个具体设备 */
    dev_t devno; /* 新字符设备的设备号:由主设备号和次设备号组成    */
    int dev_cnt; /* 次设备数量 */
    /* 2.自动创建设备节点相关成员变量 */
    struct class *class; /* 类(无论几个次设备,这里的类只有一个,当然是同一类的情况下) */
}_MY_DEV_PARENT;

_MY_DEV_PARENT parent_dev = {
    .major = 0,
    .minor = 0,
    .devno = 0,
    .dev_cnt = CHRDEV_CNT,
    .class = NULL,
};

typedef struct __CHAR_DEVICE
{
    dev_t dev_num;         // 定义 dev_t 类型(32 位大小)的变量 dev_num, 用来存放设备号
    int major;             // 主设备号:占高 12 位,用来表示驱动程序相同的一类设备,Linux 系统中主设备号范围为 0~4095
    int minor;             // 次设备号:占低 20 位,用来表示被操作的哪个具体设备
    struct cdev s_cdev;    // 定义 cdev 结构体类型的变量 scdev
    struct class *class;   // 定于 struct class *类型结构体变量 class,表示要创建的类
    struct device *device; // 设备
    char buf[BUFSIZE];     // 设置数据存储数组 mem
} _CHAR_DEVICE;

_CHAR_DEVICE g_sdev[CHRDEV_CNT];  //定义一个 g_sdev 结构体变量

static int sdev_open(struct inode *inode, struct file *file)
{
    printk("This is sdev_open!\n");
    file->private_data = (void *)(container_of(inode->i_cdev, _CHAR_DEVICE, s_cdev));
    return 0;
}

static ssize_t sdev_read(struct file *file, char __user *buf, size_t size, loff_t *off)
{
    int i = 0;
    loff_t offset = *off; // 将读取数据的偏移量赋值给 loff_t 类型变量 p
    size_t count = size;
    _CHAR_DEVICE *pDev=(_CHAR_DEVICE *)file->private_data; //在 write 函数中读取 private_data

    if (offset > BUFSIZE)
    {
        return 0;
    }

    if (count > BUFSIZE - offset)
    {
        count = BUFSIZE - offset;
    }

    if (copy_to_user(buf, pDev->buf + offset, count))
    { 
        // 将 mem 中的值写入 buf,并传递到用户空间
        printk("copy_to_user error!\n");
        return -1;
    }

    for (i = 0; i < BUFSIZE; i++)
    {
        printk("buf[%d] %c\n", i, pDev->buf[i]); // 将 mem 中的值打印出来
    }
    printk("read offset is %llu, count is %d\n", offset, count);
    *off = *off + count; // 更新偏移值

    return count;
}

static ssize_t sdev_write(struct file *file, const char __user *buf, size_t size, loff_t *off)
{
    int i = 0;
    loff_t offset = *off; // 将读取数据的偏移量赋值给 loff_t 类型变量 p
    size_t count = size;
    _CHAR_DEVICE *pDev=(_CHAR_DEVICE *)file->private_data; //在 write 函数中读取 private_data

    if (offset > BUFSIZE)
    {
        return 0;
    }

    if (count > BUFSIZE - offset)
    {
        count = BUFSIZE - offset;
    }

    if (copy_from_user(pDev->buf + offset, buf, count))
    { 
        // 将 buf 中的值,从用户空间传递到内核空间
        printk("copy_to_user error \n");
        return -1;
    }

    for (i = 0; i < BUFSIZE; i++)
    {
        printk("buf[%d] %c\n", i, pDev->buf[i]); // 将 mem 中的值打印出来
    }
    printk("write offset is %llu, count is %d\n", offset, count); // 打印写入的值
    *off = *off + count;                         // 更新偏移值

    return count;
}

static int sdev_release(struct inode *inode, struct file *file)
{
    printk("This is sdev_release!\n");
    return 0;
}

static loff_t sdev_llseek(struct file *file, loff_t offset, int whence)
{
    loff_t new_offset = 0; // 定义 loff_t 类型的新的偏移值
    switch (whence)    // 对 lseek 函数传递的 whence 参数进行判断
    {
        case SEEK_SET:
            if (offset < 0 || offset > BUFSIZE)
            {
                return -EINVAL; // EINVAL = 22 表示无效参数
            }
            new_offset = offset; // 如果 whence 参数为 SEEK_SET,则新偏移值为 offset
            break;
        case SEEK_CUR:
            if ((file->f_pos + offset < 0) || (file->f_pos + offset > BUFSIZE))
            {
                return -EINVAL;
            }
            new_offset = file->f_pos + offset; // 如果 whence 参数为 SEEK_CUR,则新偏移值为 file-> f_pos + offset,file-> f_pos 为当前的偏移值
            break;
        case SEEK_END:
            if (file->f_pos + offset < 0)
            {
                return -EINVAL;
            }
            new_offset = BUFSIZE + offset; // 如果 whence 参数为 SEEK_END,则新偏移值为 BUFSIZE + offset,BUFSIZE 为最大偏移量
            break;
        default:
            break;
    }
    file->f_pos = new_offset; // 更新 file-> f_pos 偏移值

    return new_offset;
}

static long sdev_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
    int val = 0;//定义 int 类型向应用空间传递的变量 val
    switch(cmd)
    {
        case CMD_TEST0:
            printk("this is CMD_TEST0\n");
            break;
        case CMD_TEST1:
            printk("this is CMD_TEST1\n");
            printk("arg is %ld\n",arg);//打印应用空间传递来的 arg 参数
            break;
        case CMD_TEST2:
            val = 1;//将要传递的变量 val 赋值为 1
            printk("this is CMD_TEST2\n");
            if(copy_to_user((int *)arg, &val, sizeof(val)) != 0)
            {
                //通过 copy_to_user 向用户空间传递数据
                printk("copy_to_user error \n");
            }
            break;
        case CMD_TEST3:
        {
            struct __CMD_TEST cmd_test3 = {0};
            if (copy_from_user(&cmd_test3, (int *)arg, sizeof(cmd_test3)) != 0)
            {
                printk("copy_from_user error\n");
            }
            printk("cmd_test3.a = %d\n", cmd_test3.a);
            printk("cmd_test3.b = %d\n", cmd_test3.b);
            printk("cmd_test3.c = %d\n", cmd_test3.c);
            break;
        }
        default:
            break;
    }

    return 0;
}

static struct file_operations cdev_ops = {
    .owner = THIS_MODULE,//将 owner 字段指向本模块,可以避免在模块的操作正在被使用时卸载该模块
	.open = sdev_open,
	.read = sdev_read,
	.write = sdev_write,
	.release = sdev_release,
    .llseek = sdev_llseek,
    .unlocked_ioctl = sdev_ioctl,
}; // 定义 file_operations 结构体类型的变量 cdev_ops

// 模块入口函数
static int __init chrdev_more_dev_mc_demo_init(void)
{
    int i = 0;
    int ret = 0;              // 定义 int 类型的变量 ret,用来判断函数返回值
    printk("*** [%s:%d]Build Time: %s %s, git version:%s ***\n", __FUNCTION__,
           __LINE__, KERNEL_KO_DATE, KERNEL_KO_TIME, KERNEL_KO_VERSION);
    printk("chrdev_more_dev_mc_demo module init!\n");

    ret = alloc_chrdev_region(&parent_dev.devno, 0, parent_dev.dev_cnt, CHRDEV_NAME); // 自动获取设备号,设备名为 chrdev_name
    if (ret < 0)
    {
        printk("alloc_chrdev_region is error!\n");
    }
    printk("alloc_register_region is ok!\n");
    parent_dev.major = MAJOR(parent_dev.devno); // 使用 MAJOR()函数获取主设备号
    parent_dev.minor = MINOR(parent_dev.devno); // 使用 MINOR()函数获取次设备号
    printk("parent_dev major is %d, minor is %d !\n", parent_dev.major, parent_dev.minor);
    for(i = 0; i < parent_dev.dev_cnt; i++)
    {
        g_sdev[i].dev_num = parent_dev.devno + i; /* devno + i 即可得到连续申请的设备号 */
        g_sdev[i].major = MAJOR(g_sdev[i].dev_num); /* 获取分配号的主设备号 */
        g_sdev[i].minor = MINOR(g_sdev[i].dev_num); /* 获取分配号的次设备号 */

        cdev_init(&g_sdev[i].s_cdev, &cdev_ops); // 使用 cdev_init()函数初始化 g_sdev.s_cdev 结构体,并链接到 cdev_ops 结构体
        g_sdev[i].s_cdev.owner = THIS_MODULE;    // 将 owner 字段指向本模块,可以避免在模块的操作正在被使用时卸载该模块
        ret = cdev_add(&g_sdev[i].s_cdev, g_sdev[i].dev_num, 1); // 使用 cdev_add()函数进行字符设备的添加
        if (ret < 0)
        {
            printk("cdev_add is error !\n");
        }
        printk("cdev_add dev %d is ok !\n", i);
    }
    //这几个设备都属于同一类,创建一个 class
    parent_dev.class = class_create(THIS_MODULE, CLASS_NAME); // 使用 class_create 进行类的创建,类名称为 class_dev
    //创建设备
    for (i = 0; i < parent_dev.dev_cnt; i++)
    {
        g_sdev[i].class = parent_dev.class;//这 parent_dev.dev_cnt 个设备属于同一个类
        g_sdev[i].device = device_create(g_sdev[i].class, NULL, g_sdev[i].dev_num, NULL, "%s%d", DEVICE_NAME, i); // 使用 device_create 进行设备的创建,设备名称为 device_dev
    }
    return 0;
}

// 模块出口函数
static void __exit chrdev_more_dev_mc_demo_exit(void)
{
    int i = 0;
    // 需要注意的是, 字符设备的注册要放在申请字符设备号之后,
    // 字符设备的删除要放在释放字符驱动设备号之前。
    for (i = 0; i < parent_dev.dev_cnt; i++)
    {
        /* 删除的是 /dev/dev_name */
        device_destroy(parent_dev.class, g_sdev[i].dev_num);
        printk("g_sdev[%d]: [/dev/%s%d] has been deleted!\n", i, DEVICE_NAME, i);
    }
	// 删除的是 /sys/class/class_name
    class_destroy(parent_dev.class);           // 删除创建的类

    /* 删除字符设备对象 cdev */
    for (i = 0; i < parent_dev.dev_cnt; i++)
    {
        cdev_del(&g_sdev[i].s_cdev);// 使用 cdev_del()函数进行字符设备的删除
    }

    /* 注销设备号(将会删除 /proc/devices 中的设备号和对应的名称记录) */
    unregister_chrdev_region(parent_dev.devno, parent_dev.dev_cnt);
    printk("chrdev_more_dev_mc_demo exit!\n");
}

module_init(chrdev_more_dev_mc_demo_init); // 将__init 定义的函数指定为驱动的入口函数
module_exit(chrdev_more_dev_mc_demo_exit); // 将__exit 定义的函数指定为驱动的出口函数

/* 模块信息(通过 modinfo chrdev_more_dev_mc_demo 查看) */
MODULE_LICENSE("GPL v2");            /* 源码的许可证协议 */
MODULE_AUTHOR("sumu");               /* 字符串常量内容为模块作者说明 */
MODULE_DESCRIPTION("Description");   /* 字符串常量内容为模块功能说明 */
MODULE_ALIAS("module's other name"); /* 字符串常量内容为模块别名 */

3. 开发板测试

  • (1)加载驱动
shell
insmod chrdev_more_dev_mc_demo.ko
  • (2)查看设备节点
shell
ls /dev/sdevice* -alh

会看到有如下打印信息:

shell
crw-rw----    1 0        root      246,   0 Jan  1 00:00 /dev/sdevice0
crw-rw----    1 0        root      246,   1 Jan  1 00:00 /dev/sdevice1

在创建设备节点的时候命名就是 /dev/sdeviceN,所以这里是 0 和 1 两个设备节点,他们会对应不同的缓冲区。

  • (3)向设备节点写入数据
shell
echo 123456789 > /dev/sdevice0
echo 987654321 > /dev/sdevice1

# 或者使用应用测试程序
./xxx_demo_app.out /dev/sdevice 1 x # 从 x 位置读取
./xxx_demo_app.out /dev/sdevice 2 x # 从 x 位置写入
./xxx_demo_app.out /dev/sdevice 3 x # x 为任意值
  • (4)从设备节点读取数据
shell
cat /dev/sdevice0
cat /dev/sdevice1

# 或者使用应用测试程序
./xxx_demo_app.out /dev/sdevice 1 x # 从 x 位置读取
./xxx_demo_app.out /dev/sdevice 2 x # 从 x 位置写入
./xxx_demo_app.out /dev/sdevice 3 x # x 为任意值

参考资料

【1】Linux 内核宏 Container_Of 的详细解释_Linux_脚本之家

【2】【Linux API 揭秘】container_of 函数详解 - 董哥聊技术 - 博客园

【3】Typeof (Using the GNU Compiler Collection (GCC))

【4】linux 内核宏 container_of 剖析 - 知乎