Linux 字符设备驱动的编写

Linux 字符设备驱动的编写

作者:解琛
时间:2020 年 8 月 17 日

字符设备驱动

一、Linux 设备分类

Linux 中,根据设备的类型可以分为三类:字符设备、块设备和网络设备。

  • 字符设备:应用程序按字节/字符来读写数据,通常不支持随机存取。我们常用的键盘、串口都是字符设备。
  • 块设备:应用程序可以随机访问设备数据。典型的块设备有硬盘、SD卡、闪存等,应用程序 可以寻址磁盘上的任何位置,并由此读取数据。此外,数据的读写只能以块的倍数进行。
  • 网络设备:是一种特殊设备,它并不存在于/dev下面,主要用于网络数据的收发。

二、open()

Linux基础命令—mknod

在使用 C 库中 open() 函数打开设备文件时,执行了以下操作。
open函数

 mknod [选项]  设备名  设备类型  主设备号 次设备号

设备文件通常在开机启动时自动创建的,可以使用命令 mknod 来创建一个新的设备文件。

static struct inode *shmem_get_inode( struct super_block *sb, const struct inode *dir,
                                      umode_t mode, dev_t dev, unsigned long flags)
{
    inode = new_inode(sb);
    if (inode) {
        ......
        switch (mode & S_IFMT) {
            default:
                inode->i_op = &shmem_special_inode_operations;
                init_special_inode(inode, mode, dev);
                break;
        ......
        }
    } else
        shmem_free_inode(sb);
        return inode;
}
 
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;
    }
    ....
}

使用 mknod 命令,创建了一个字符设备文件时,实际上就是创建了一个设备节点 inode 结构体,并且将该设备的设备编号记录在成员 i_rdev,将成员 f_op 指针指向了 def_chr_fops 结构体。

命令 mknod 最终会调用 init_special_inode 函数。

使用的 open 函数在内核中对应的是 sys_open 函数,sys_open 函数会调用do_sys_open函数。

在 do_sys_open 函数中,首先调用函数 get_unused_fd_flags 来获取一个未被使用的文件描述符 fd,该文件描述符就是最终通过 open 函数得到的值。

紧接着,调用 do_filp_open 函数,该函数通过调用函数 get_empty_filp 得到一个新的 file 结构体,之后开始解析文件路径,查找该文件的文件节点 inode 等,接着来到了函数 do_dentry_open 函数。

static int do_dentry_open(  struct file *f,
                            struct inode *inode,
                            int (*open)(struct inode *, struct file *),
                            const struct cred *cred)
{
    ……
    f->f_op = fops_get(inode->i_fop);
    ……
    if (!open)
        open = f->f_op->open;
    if (open) {
        error = open(inode, f);
        if (error)
            goto cleanup_all;
    }
    ……
}

在该函数的实现中,使用 fops_get 函数来获取该文件节点 inode 的成员变量 i_fop。

使用 mknod 创建字符设备文件时,将 def_chr_fops 结构体赋值给了该设备文件 inode 的 i_fop 成员。

到了这里,新建的 file 结构体的成员 f_op 就指向了 def_chr_fops。

/* def_chr_fops 结构体(位于 内核源码/fs/char_dev.c 文件) */

const struct file_operations def_chr_fops = {
    .open = chrdev_open,
    .llseek = noop_llseek,
};

chrdev_open实现示意图

最终,会执行 def_chr_fops 中的 open 函数,也就是 chrdev_open 函数。

chrdev_open 可以理解为一个字符设备的通用初始化函数,根据字符设备的设备号,找到相应的字符设备,从而得到操作该设备的方法。

最后,调用上图的fd_install函数,完成文件描述符和文件结构体 file 的关联。

之后使用对该文件描述符 fd 调用 read、write 函数,最终都会调用 file 结构体对应的函数,实际上也就是调用 cdev 结构体中 ops 结构体内的相关函数。

总结以下,当使用 open 函数,打开设备文件时:会根据该设备的文件的设备号找到相应的设备结构体,从而得到了操作该设备的方法。

也就是说如果要添加一个新设备的话,需要提供:

  1. 一个设备号;
  2. 一个设备结构体;
  3. 一组操作该设备的方法(file_operations结构体)。

三、数据结构

3.1 struct file_operations

file_operations 结构体中包含了操作文件的一系列函数指针。

/* file_operations 结构体(位于 内核源码/include/linux/fs.h 文件) */

struct file_operations {
    loff_t (*llseek) (struct file *, loff_t, int);
    ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
    ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
    long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
    int (*open) (struct inode *, struct file *)
    int (*release) (struct inode *, struct file *);
};
  • llseek:用于修改文件的当前读写位置,并返回偏移后的位置;
    • file:用于传入对应的文件指针,通常用于读取文件的信息,如文件类型、读写权限;
    • loff_t:指定偏移量的大小;
    • int:是用于指定新位置,指定成从文件的某个位置进行偏移;
      • SEEK_SET:表示从文件起始处开始偏移;
      • SEEK_CUR:表示从当前位置开始偏移;
      • SEEK_END:表示从文件结尾开始偏移;
  • read:用于读取设备中的数据,并返回成功读取的字节数;
    • file:类型指针变量;
    • __user*:类型的数据缓冲区,__user用于修饰变量,表明该变量所在的地址空间是用户空间的。内核模块不能直接使用该数据,需要使用 copy_to_user 函数来进行操作;
    • size_t:类型变量指定读取的数据大小;
  • write:用于向设备写入数据,并返回成功写入的字节数;
  • unlocked_ioctl:提供设备执行相关控制命令的实现方法,它对应于应用程序的 fcntl 函数以及 ioctl 函数。在 kernel 3.0 中已经完全删除了 struct file_operations 中的 ioctl 函数指针;
  • open:设备驱动第一个被执行的函数,一般用于硬件的初始化。如果该成员被设置为 NULL,则表示这个设备的打开操作永远成功;
  • release:当 file 结构体被释放时,将会调用该函数。与 open 函数相反,该函数可以用于释放。
/* copy_to_user 和 copy_from_user 函数(位于 内核源码/include/asm-generic/uaccess.h 文件) */

static inline long copy_from_user(  void *to,
                                    const void __user * from, 
                                    unsigned long n )

static inline long copy_to_user(    void __user *to,
                                    const void *from, 
                                    unsigned long n )

使用 read 和 write 函数时,需要使用 copy_to_user 函数以及 copy_from_user 函数来进行数据访问,写入 / 读取成功函数返回 0,失败则会返回未被拷贝的字节数。

  • to:指定目标地址,也就是数据存放的地址;
  • from:指定源地址,也就是数据的来源;
  • n:指定写入/读取数据的字节数。

3.2 struct file

内核中用 file 结构体来表示每个打开的文件,每打开一个文件,内核会创建一个结构体,并将对该文件上的操作函数传递给该结构体的成员变量 f_op。

/* file 结构体(位于 内核源码/include/fs.h 文件)*/

struct file {
    const struct file_operations *f_op;
   
    /* needed for tty driver, and maybe others */
    void *private_data;
};
  • f_op:存放与文件操作相关的一系列函数指针,如open、read、wirte等函数;
  • private_data:该指针变量只会用于设备驱动程序中,内核并不会对该成员进行操作。因此,在驱动程序中,通常用于指向描述设备的结构体。

3.3 struct cdev

cdev

内核用 struct cdev 结构体来描述一个字符设备,并通过 struct kobj_map 类型的散列表 cdev_map 来管理当前系统中的所有字符设备。

/* cdev 结构体(位于 内核源码/include/linux/cdev.h 文件)*/

struct cdev {
    struct kobject kobj;
    struct module *owner;
    const struct file_operations *ops;
    struct list_head list;
    dev_t dev;
    unsigned int count;
};
  • kobj:内核数据对象,用于管理该结构体。obj_lookup 函数中从 cdev_map 中得到该成员,由该成员便可以得到相应的字符设备结构体;
  • owner:指向了关联该设备的内核模块,实际上就是关联了驱动程序,通常设置为 THIS_MODULE;
  • ops:该结构体中最重要的一部分,也是我们实现字符设备驱动的关键一步,用于存放所有操作该设备的函数指针;
  • list:实现一个链表,用于包含与该结构体对应的字符设备文件 inode 的成员 i_devices 的链表;
  • dev:记录了字符设备的设备号;
  • count:记录了与该字符设备使用的次设备号的个数;

四、字符设备驱动程序框架

Linux 给开发者提供了一个基本的框架,如果你不按照这个框架写驱动,那么编写的驱动程序就不能被内核所接纳的。

4.1 初始化字符设备

/* 法一 */
static struct cdev chrdev;

/* 法二 */
struct cdev *cdev_alloc(void);
  • 第一种方式:就是我们常见的变量定义;
  • 第二种方式:是内核提供的动态分配方式,调用该函数之后,会返回一个struct cdev类型的指针,用于描述字符设备。

4.2 移除字符设备

void cdev_del(struct cdev *p)

4.3 分配设备号

Linux 的各种设备都以文件的形式存放在 /dev 目录下,为了管理这些设备,系统为各个设备进行编号。

每个设备号又分为主设备号和次设备号。

  • 主设备号用来 区分不同种类的设备,如USB,tty等;
  • 次设备号用来区分同一类型的多个设备,如tty0,tty1等。

内核提供了一种数据类型:dev_t,用于记录设备编号。

该数据类型实际上是一个无符号 32 位整型,其中的 12 位用于表示主设备号,剩余的 20 位则用于表示次设备号。

内核将一部分主设备号分配给了一些常见的设备。在内核源码的 Documentation/devices.txt 文件中可以找到这些设备以及这部分设备占据的主设备号。
设备号文件

  1. 记录了当前内核所占据的所有字符设备的主设备号,通过检查这一列的内容,便可以知道当前的主设备号是否被内核占用;
  2. 记录了设备的类型,主要分为块设备(block)以及字符设备(char);
  3. 记录了每个次设备号对应的设备;
  4. 对每个设备的概述。

创建一个新的字符设备之前,需要为新的字符设备注册一个新的设备号,内核提供了三种方式,来完成这项工作。

4.3.1 register_chrdev_region

register_chrdev_region 函数用于静态地为一个字符设备申请一个或多个设备编号。

该函数在分配成功时,会返回0;失败则会返回相应的错误码。

/* (位于 内核源码/fs/char_dev.c) */

int register_chrdev_region(dev_t from, unsigned count, const char *name)
  • from:dev_t 类型的变量,用于指定字符设备的起始设备号;
  • count:指定要申请的设备号个数;
  • name:用于指定该设备的名称,可以在 /proc/devices 中看到该设备。

register_chrdev_region 函数使用时需要指定一个设备编号,Linux 内核为我们提供了生成设备号的宏定义。

/* 合成设备号 MKDEV(位于 内核源码/include/linux/kdev_t.h)*/

#define MINORBITS 20
#define MINORMASK ((1U << MINORBITS) - 1)
#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))
#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))
#define MKDEV(ma,mi) (((ma) << MINORBITS) \| (mi))
  • MKDEV:用于将主设备号和次设备号合成一个设备号,主设备可以通过查阅内核源码的 Documentation/devices.txt 文件,而次设备号通常是从编号 0 开始;
  • MAJOR:根据设备的设备号来获取设备的主设备号;
  • MINOR:根据设备的设备号来获取设备的次设备号。

4.3.2 alloc_chrdev_region

调用 alloc_chrdev_region函数,内核会自动分配给我们一个尚未使用的主设备号。

通过命令 cat /proc/devices 查询内核分配的主设备号。

/* (位于 内核源码/fs/char_dev.c) */

int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
  • dev:指向dev_t类型数据的指针变量,用于存放分配到的设备编号的起始值;
  • baseminor:次设备号的起始值,通常情况下,设置为0;
  • count:指定要申请的设备号个数;
  • name:用于指定该设备的名称,可以在 /proc/devices 中看到该设备。

4.3.3 register_chrdev

内核提供了 register_chrdev 函数用于分配设备号。

该函数是一个内联函数,不仅支持静态申请设备号,也支持动态申请设备号,并将主设备号返回。

/*  位于 内核源码/include/linux/fs.h 文件 */

static inline int register_chrdev(  unsigned int major, 
                                    const char *name,
                                    const struct file_operations *fops )
{
    return __register_chrdev(major, 0, 256, name, fops);
}
  • major:用于指定要申请的字符设备的主设备号,等价于 register_chrdev_region 函数,当设置为 0 时,内核会自动分配一个未使用的主设备号;
  • name:用于指定字符设备的名称;
  • fops:用于操作该设备的函数接口指针。

使用 register_chrdev 函数向内核申请设备号,同一类字符设备(即主设备号相同),会在内核中申请了 256 个,通常情况下,开发者不需要用到这么多个设备,这就造成了极大的资源浪费

4.4 注销设备号

4.4.1 unregister_chrdev_region

当删除字符设备时候,需要把分配的设备编号交还给内核,对于使用 register_chrdev_region 函数以及 alloc_chrdev_region 函数分配得到的设备编号,可以使用 unregister_chrdev_region 函数实现该功能。

/* (位于 内核源码/fs/char_dev.c) */

void unregister_chrdev_region(dev_t from, unsigned count)
  • from:指定需要注销的字符设备的设备编号起始值,一般将定义的 dev_t 变量作为实参;
  • count:指定需要注销的字符设备编号的个数,该值应与申请函数的 count 值相等,通常采用宏定义进行管理。

4.4.2 unregister_chrdev

使用 register_chrdev 函数申请的设备号,则应该使用 unregister_chrdev 函数进行注销。

/* 位于 内核源码/include/linux/fs.h 文件 */

static inline void unregister_chrdev(unsigned int major, const char *name)
{
    __unregister_chrdev(major, 0, 256, name);
}
  • major:指定需要释放的字符设备的主设备号,一般使用 register_chrdev 函数的返回值作为实参;
  • name:执行需要释放的字符设备的名称。

4.5 关联设备的操作方式

实现 file_operations 这个结构体中的函数之后,需要将该结构体与字符设备进行关联,内核提供了 cdev_init 函数来实现该功能。

/* /fs/char_dev.c */<++>

void cdev_init(struct cdev *cdev, const struct file_operations *fops)
  • cdev:struct cdev 类型的指针变量,指向需要关联的字符设备结构体;
  • fops:file_operations 类型的结构体指针变量,一般将实现操作该设备的结构体 file_operations 结构体作为实参;

4.6 注册设备

cdev_add 函数用于向内核的 cdev_map 散列表添加一个新的字符设备。

/* /fs/char_dev.c */

int cdev_add(struct cdev *p, dev_t dev, unsigned count)
  • p:struct cdev 类型的指针,用于指定需要添加的字符设备;
  • dev:dev_t 类型变量,用于指定设备的起始编号;
  • count:指定注册多少个设备。

五、案例

首先,字符设备驱动程序是以内核模块的形式存在的,因此,使用内核模块的程序框架是毫无疑问的。

紧接着,要向系统注册一个新的字符设备,需要这几样东西:

  1. 字符设备结构体cdev;
  2. 设备编号devno;
  3. 操作方式结构体file_operations。

5.1 编写内核模块

编写一个 Linux 内核模块

实验环境如下。

jerome@jerome:~$ uname -a
Linux jerome 5.4.0-42-generic #46~18.04.1-Ubuntu SMP Fri Jul 10 07:21:24 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux
jerome@jerome:~$ lsb_release -a
No LSB modules are available.
Distributor ID:	Ubuntu
Description:	Ubuntu 18.04.4 LTS
Release:	18.04
Codename:	bionic

5.1.1 加载模块

#define DEV_NAME "JeromeCharDev"    /* 设备名称;*/
#define DEV_CNT (1)                 /* 设备数量;*/

/* 定义字符设备的设备号;*/
static dev_t devno;

/* 定义字符设备结构体 chr_dev; */
static struct cdev chr_dev;

static int __init chrdev_init(void)
{
    int ret = 0;
    printk("chrdev init\n");

    // 第一步;
    // 采用动态分配的方式,获取设备编号,次设备号为0;
    // 设备名称为 JeromeCharDev,可通过命令 cat /proc/devices 查看;
    // DEV_CNT 为 1,当前只申请一个设备编号;
    ret = alloc_chrdev_region(&devno, 0, DEV_CNT, DEV_NAME);

    if (ret < 0) {
        printk("fail to alloc devno\n");
        goto alloc_err; 
        /* 当获取失败时,直接返回对应的错误码;*/
    }

    // 第二步;
    // 关联字符设备结构体 cdev 与文件操作结构体 file_operations;
    cdev_init(&chr_dev, &chr_dev_fops);

    // 第三步;
    // 添加设备至 cdev_map 散列表中;
    ret = cdev_add(&chr_dev, devno, DEV_CNT);
    
    if (ret < 0) {
        printk("fail to add cdev\n");
        goto add_err;
    }
    return 0;

add_err:
    // 添加设备失败时,需要注销设备号;
    unregister_chrdev_region(devno, DEV_CNT);
alloc_err:
    return ret;
}

module_init(chrdev_init);

5.1.2 卸载模块

static void __exit chrdev_exit(void)
{
    printk("chrdev exit\n");
    
    unregister_chrdev_region(devno, DEV_CNT);
    
    cdev_del(&chr_dev);
}

module_exit(chrdev_exit);

5.2 文件操作方式的实现

#define BUFF_SIZE 128

// 数据缓冲区;
static char vbuf[BUFF_SIZE];

static struct file_operations chr_dev_fops = {
    .owner      = THIS_MODULE,
    .open       = chr_dev_open,
    .release    = chr_dev_release,
    .write      = chr_dev_write,
    .read       = chr_dev_read,
};

由于这个字符设备是一个虚拟的设备,与硬件并没有什么关联,因此,open 函数与 release 直接返回 0 即可。

static int chr_dev_open(struct inode *inode, struct file *filp)
{
    printk("\nopen\n");
    return 0;
}

static int chr_dev_release(struct inode *inode, struct file *filp)
{
    printk("\nrelease\n");
    return 0;
}

在 open 函数与 release 函数中打印相关的调试信息。

static ssize_t chr_dev_write(struct file *filp, const char __user * buf, size_t count, loff_t *ppos)
{
    unsigned long p = *ppos;
    int ret;
    int tmp = count ;
    if (p > BUFF_SIZE)
        return 0;
    if (tmp > BUFF_SIZE - p)
        tmp = BUFF_SIZE - p;
    ret = copy_from_user(vbuf, buf, tmp);
    *ppos += tmp;
    return tmp;
}

当应用程序调用 write 函数,最终就调用这个 chr_dev_write 函数。

在该函数中,变量 p 记录了当前文件的读写位置,如果超过了数据缓冲区的大小( 128 字节)的话,直接返回 0。

如果要读写的数据个数超过了数据缓冲区剩余的内容的话,则只读取剩余的内容。

使用 copy_from_user 从用户空间拷贝 tmp 个字节的数据到数据缓冲区中,同时让文件的读写位置偏移同样的字节数。

static ssize_t chr_dev_read(struct file *filp, char __user * buf, size_t count, loff_t *ppos)
{
    unsigned long p = *ppos;
    int ret;
    int tmp = count ;
    if (p >= BUFF_SIZE)
        return 0;
    if (tmp > BUFF_SIZE - p)
        tmp = BUFF_SIZE - p;
    ret = copy_to_user(buf, vbuf+p, tmp);
    *ppos +=tmp;
    return tmp;
}

当应用程序调用 read 函数,则会执行 chr_dev_read 函数的内容。

与 write 函数类似,区别在于,使用 copy_to_user 从数据缓冲区拷贝 tmp 个字节的数据到用户空间中。

5.3 完整程序

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

#define DEV_NAME    "jeromeCharDev"
#define DEV_CNT     (1)
#define BUFF_SIZE   128

static dev_t devno;
static struct cdev chr_dev;
static char vbuf[BUFF_SIZE];

static int chr_dev_open(struct inode *inode, struct file *filp);
static int chr_dev_release(struct inode *inode, struct file *filp);
static ssize_t chr_dev_write(struct file *filp, const char __user * buf, size_t count, loff_t *ppos);
static ssize_t chr_dev_read(struct file *filp, char __user * buf, size_t count, loff_t *ppos);

static struct file_operations  chr_dev_fops =
{
    .owner = THIS_MODULE,
    .open = chr_dev_open,
    .release = chr_dev_release,
    .write = chr_dev_write,
    .read = chr_dev_read,
};

static int chr_dev_open(struct inode *inode, struct file *filp)
{
    printk("\nopen\n");
    return 0;
}

static int chr_dev_release(struct inode *inode, struct file *filp)
{
    printk("\nrelease\n");
    return 0;
}

static ssize_t chr_dev_write(struct file *filp, const char __user * buf, size_t count, loff_t *ppos)
{
    unsigned long p = *ppos;
    int ret;
    int tmp = count ;
    if(p > BUFF_SIZE)
        return 0;
    if(tmp > BUFF_SIZE - p)
        tmp = BUFF_SIZE - p;
    ret = copy_from_user(vbuf, buf, tmp);
    *ppos += tmp;
    return tmp;
}

static ssize_t chr_dev_read(struct file *filp, char __user * buf, size_t count, loff_t *ppos)
{
    unsigned long p = *ppos;
    int ret;
    int tmp = count ;
    static int i = 0;
    i++;
    if(p >= BUFF_SIZE)
        return 0;
    if(tmp > BUFF_SIZE - p)
        tmp = BUFF_SIZE - p;
    ret = copy_to_user(buf, vbuf+p, tmp);
    *ppos +=tmp;
    return tmp;
}

static int __init chrdev_init(void)
{
    int ret = 0;
    printk("chrdev init\n");
    ret = alloc_chrdev_region(&devno, 0, DEV_CNT, DEV_NAME);
    if(ret < 0){
        printk("fail to alloc devno\n");
        goto alloc_err;
    }
    cdev_init(&chr_dev, &chr_dev_fops);
    ret = cdev_add(&chr_dev, devno, DEV_CNT);
    if(ret < 0)
    {
        printk("fail to add cdev\n");
        goto add_err;
    }
    return 0;

add_err:
    unregister_chrdev_region(devno, DEV_CNT);
alloc_err:
    return ret;
}
module_init(chrdev_init);

static void __exit chrdev_exit(void)
{
    printk("chrdev exit\n");
    unregister_chrdev_region(devno, DEV_CNT);

    cdev_del(&chr_dev);
}
module_exit(chrdev_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("xiechen");

5.4 Makefile

obj-m := jeromeDev.o      

KERNELBUILD :=/lib/modules/$(shell uname -r)/build  # 编译内核模块需要的Makefile的路径,Ubuntu下是 /lib/modules/5.4.0-42-generic-generic/build;

default:  
    make -C $(KERNELBUILD) M=$(shell pwd) modules   # 编译内核模块。-C 将工作目录转到 KERNELBUILD,调用该目录下的 Makefile,并向这个 Makefile 传递参数 M 的值是 $(shell pwd) modules;

clean:  
    rm -rf *.o *.ko *.mod.c .*.cmd *.markers *.order *.symvers .tmp_versions

5.5 编译装载内核模块

文件树如下。

xiechen@xiechen-Ubuntu:~/文档/1.linux/0.内核/0.字符设备驱动$ tree
.
├── jeromeDev.c
└── Makefile

0 directories, 2 files

对字符驱动的内核模块进行编译。

make

编译好内核模块之后的文件树如下。

xiechen@xiechen-Ubuntu:~/文档/1.linux/0.内核/0.字符设备驱动$ make
make -C /lib/modules/5.4.0-42-generic/build   M=/home/xiechen/文档/1.linux/0.内核/0.字符设备驱动 modules   # 编译内核模块。-C 将工作目录转到 KERNELBUILD,调用该目录下的 Makefile,并向这个 Makefile 传递参数 M 的值是 /home/xiechen/文档/1.linux/0.内核/0.字符设备驱动 modules;
make[1]: 进入目录“/usr/src/linux-headers-5.4.0-42-generic”
  CC [M]  /home/xiechen/文档/1.linux/0.内核/0.字符设备驱动/jeromeDev.o
  Building modules, stage 2.
  MODPOST 1 modules
  CC [M]  /home/xiechen/文档/1.linux/0.内核/0.字符设备驱动/jeromeDev.mod.o
  LD [M]  /home/xiechen/文档/1.linux/0.内核/0.字符设备驱动/jeromeDev.ko
make[1]: 离开目录“/usr/src/linux-headers-5.4.0-42-generic”
xiechen@xiechen-Ubuntu:~/文档/1.linux/0.内核/0.字符设备驱动$ tree
.
├── jeromeDev.c
├── jeromeDev.ko
├── jeromeDev.mod
├── jeromeDev.mod.c
├── jeromeDev.mod.o
├── jeromeDev.o
├── Makefile
├── modules.order
└── Module.symvers

0 directories, 9 files

装载内核模块。

sudo insmod jeromeDev.ko

查看内核信息。

xiechen@xiechen-Ubuntu:~/文档/1.linux/0.内核/0.字符设备驱动$ dmesg
[ 9836.821412] chrdev init

查看字符设备,注册的设备主设备号为 240。

xiechen@xiechen-Ubuntu:~/文档/1.linux/0.内核/0.字符设备驱动$ cat /proc/devices
Character devices:
240 jeromeCharDev

使用 mknod 命令来创建一个新的设备 chrdev。

xiechen@xiechen-Ubuntu:~/文档/1.linux/0.内核/0.字符设备驱动$ sudo mknod /dev/chrdev c 240 0
xiechen@xiechen-Ubuntu:~/文档/1.linux/0.内核/0.字符设备驱动$ sudo ls -l /dev/chrdev
crw-r--r-- 1 root root 240, 0 8月  17 11:54 /dev/chrdev

5.6 应用程序

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
char *wbuf = "Hello World\n";
char rbuf[128];
int main(void)
{
    printf("JeromeCharDev test\n");

    // 打开文件;
    int fd = open("/dev/chrdev", O_RDWR);

    // 写入数据;
    write(fd, wbuf, strlen(wbuf));

    // 写入完毕,关闭文件;
    close(fd);

    // 打开文件;
    fd = open("/dev/chrdev", O_RDWR);

    // 读取文件内容;
    read(fd, rbuf, 128);

    // 打印读取的内容;
    printf("The content : %s", rbuf);

    // 读取完毕,关闭文件;
    close(fd);

    return 0;
}

文件树如下。

xiechen@xiechen-Ubuntu:~/文档/1.linux/0.内核/0.字符设备驱动$ tree
.
├── jeromeDev.c
├── jeromeDev.ko
├── jeromeDev.mod
├── jeromeDev.mod.c
├── jeromeDev.mod.o
├── jeromeDev.o
├── main.c
├── Makefile
├── modules.order
└── Module.symvers

0 directories, 10 files

对应用程序进行编译。

gcc -o main main.c

运行该程序,实验结果如下。

xiechen@xiechen-Ubuntu:~/文档/1.linux/0.内核/0.字符设备驱动$ sudo ./main 
JeromeCharDev test
The content : Hello World

5.7 echo / cat 测试

xiechen@xiechen-Ubuntu:~/文档/1.linux/0.内核/0.字符设备驱动$ sudo echo “JeromeCharDev test” > sudo /dev/chrdev
xiechen@xiechen-Ubuntu:~/文档/1.linux/0.内核/0.字符设备驱动$ cat /dev/chrdev
Hello World

5.8 卸载内核模块

sudo rmmod jeromeDev.ko

5.9 删除设备文件

sudo rm /dev/chrdev

六、使用 private_data 进行多设备支持

在 Linux 内核中,主设备号用于标识设备对应的驱动程序,告诉 Linux 内核使用哪一个驱动程序为该设备服务。

次设备号表示了同类设备的各个设备,每个设备的功能都是不一样的。

对案例中的程序进行修改,让该驱动对多设备进行支持,主要有两种方法。

  1. 根据次设备号,来区分各种设备;
  2. file 结构体的私有数据成员 private_data。

6.1 修改设备数

#define DEV_CNT (2) 

通过修改 DEV_CNT 将设备个数设为 2,使驱动程序可以管理两个设备。

6.2 增加设备缓冲区

static char vbuf1[BUFF_SIZE]; 
static char vbuf2[BUFF_SIZE]; 

修改 vbuf,定义两个数据缓冲区。

6.3 修改缓冲区指向

static int chr_dev_open(struct inode *inode, struct file *filp)
{
    printk("\nopen\n ");
    switch (MINOR(inode->i_rdev)) {
        case 0 : {
            filp->private_data = vbuf1;
            break;
        }

        case 1 : {
            filp->private_data = vbuf2;
            break;
        }
    }
    return 0;
}

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

在 chr_dev_open 函数中,我们使用宏定义 MINOR 来获取该设备文件的次设备号,使用 private_data 指向各自的数据缓冲区。

对于次设备号为 0 的设备,负责管理 vbuf1 的数据,对于次设备号为 1 的设备,则用于管理 vbuf2 的数据,这样就实现了同一个设备驱动管理多个设备了。

6.4 对 private_data 进行读写

static ssize_t chr_dev_write(struct file *filp, const char __user * buf, size_t count, loff_t *ppos)
{
    unsigned long p = *ppos;
    int ret;
    char *vbuf = filp->private_data;
    int tmp = count ;
    if (p > BUFF_SIZE)
        return 0;
    if (tmp > BUFF_SIZE - p)
        tmp = BUFF_SIZE - p;
    ret = copy_from_user(vbuf, buf, tmp);
    *ppos += tmp;
    return tmp;
}

将原先 vbuf 数据指向了 private_data,这样的话,当用户往次设备号为 0 的设备写数据时,就会往 vbuf1 中写入数据。

static ssize_t chr_dev_read(struct file *filp, char __user * buf, size_t count, loff_t *ppos)
{
    unsigned long p = *ppos;
    int ret;
    int tmp = count ;
    char *vbuf = filp->private_data;
    if (p >= BUFF_SIZE)
        return 0;
    if (tmp > BUFF_SIZE - p)
        tmp = BUFF_SIZE - p;
    ret = copy_to_user(buf, vbuf+p, tmp);
    *ppos +=tmp;
    return tmp;
}

6.5 完整源码

#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#define DEV_NAME            "jeromeCharDev"
#define DEV_CNT                 (2)
#define BUFF_SIZE               128
static dev_t devno;
static struct cdev chr_dev;
static char vbuf1[BUFF_SIZE];
static char vbuf2[BUFF_SIZE];
static int chr_dev_open(struct inode *inode, struct file *filp);
static int chr_dev_release(struct inode *inode, struct file *filp);
static ssize_t chr_dev_write(struct file *filp, const char __user * buf, size_t count, loff_t *ppos);
static ssize_t chr_dev_read(struct file *filp, char __user * buf, size_t count, loff_t *ppos);
static struct file_operations  chr_dev_fops = 
{
    .owner = THIS_MODULE,
    .open = chr_dev_open,
    .release = chr_dev_release,
    .write = chr_dev_write,
    .read = chr_dev_read,
};

static int chr_dev_open(struct inode *inode, struct file *filp)
{
    printk("\nopen\n ");
    switch(MINOR(inode->i_rdev))
    {
        case 0 :
        {
            filp->private_data = vbuf1;
            break;
        }
        case 1 :
        {
            filp->private_data = vbuf2;
            break;
        }
    }
    return 0;
}

static int chr_dev_release(struct inode *inode, struct file *filp)
{
    printk("\nrelease\n");
    return 0;
}

static ssize_t chr_dev_write(struct file *filp, const char __user * buf, size_t count, loff_t *ppos)
{
    unsigned long p = *ppos;
    int ret;
    char *vbuf = filp->private_data;
    int tmp = count ;
    if(p > BUFF_SIZE)
        return 0;
    if(tmp > BUFF_SIZE - p)
        tmp = BUFF_SIZE - p;
    ret = copy_from_user(vbuf, buf, tmp);
    *ppos += tmp;
    return tmp;
}

static ssize_t chr_dev_read(struct file *filp, char __user * buf, size_t count, loff_t *ppos)
{
    unsigned long p = *ppos;
    int ret;
    int tmp = count ;
    char *vbuf = filp->private_data;
    if(p >= BUFF_SIZE)
        return 0;
    if(tmp > BUFF_SIZE - p)
        tmp = BUFF_SIZE - p;
    ret = copy_to_user(buf, vbuf+p, tmp);
    *ppos +=tmp;
    return tmp;
}

static int __init chrdev_init(void)
{
    int ret = 0;
    printk("chrdev init\n");
    ret = alloc_chrdev_region(&devno, 0, DEV_CNT, DEV_NAME);
    if(ret < 0){
        printk("fail to alloc devno\n");
        goto alloc_err;
    }
    cdev_init(&chr_dev, &chr_dev_fops);
    ret = cdev_add(&chr_dev, devno, DEV_CNT);
    if(ret < 0)
    {
        printk("fail to add cdev\n");
        goto add_err;
    }
    return 0;

add_err:
    unregister_chrdev_region(devno, DEV_CNT);
alloc_err:
    return ret;
}
module_init(chrdev_init);

static void __exit chrdev_exit(void)
{
    printk("chrdev exit\n");
    unregister_chrdev_region(devno, DEV_CNT);

    cdev_del(&chr_dev);
}
module_exit(chrdev_exit);

MODULE_LICENSE("GPL");

6.6 cat / echo 测试

Makefile 文件与上一部分相同。

xiechen@xiechen-Ubuntu:~/文档/1.linux/0.内核/2.字符设备驱动多设备支持$ tree
.
├── jeromeDev.c
└── Makefile

0 directories, 2 files
xiechen@xiechen-Ubuntu:~/文档/1.linux/0.内核/2.字符设备驱动多设备支持$ make
make -C /lib/modules/5.4.0-42-generic/build   M=/home/xiechen/文档/1.linux/0.内核/2.字符设备驱动多设备支持 modules   # 编译内核模块。-C 将工作目录转到 KERNELBUILD,调用该目录下的 Makefile,并向这个 Makefile 传递参数 M 的值是 /home/xiechen/文档/1.linux/0.内核/2.字符设备驱动多设备支持 modules;
make[1]: 进入目录“/usr/src/linux-headers-5.4.0-42-generic”
  CC [M]  /home/xiechen/文档/1.linux/0.内核/2.字符设备驱动多设备支持/jeromeDev.o
  Building modules, stage 2.
  MODPOST 1 modules
  CC [M]  /home/xiechen/文档/1.linux/0.内核/2.字符设备驱动多设备支持/jeromeDev.mod.o
  LD [M]  /home/xiechen/文档/1.linux/0.内核/2.字符设备驱动多设备支持/jeromeDev.ko
make[1]: 离开目录“/usr/src/linux-headers-5.4.0-42-generic”
xiechen@xiechen-Ubuntu:~/文档/1.linux/0.内核/2.字符设备驱动多设备支持$ sudo insmod jeromeDev.ko
xiechen@xiechen-Ubuntu:~/文档/1.linux/0.内核/2.字符设备驱动多设备支持$ cat /proc/devices 
Character devices:
240 jeromeCharDev
xiechen@xiechen-Ubuntu:~/文档/1.linux/0.内核/2.字符设备驱动多设备支持$ sudo mknod /dev/chrdev1 c 240 0
xiechen@xiechen-Ubuntu:~/文档/1.linux/0.内核/2.字符设备驱动多设备支持$ sudo mknod /dev/chrdev2 c 240 1

通过以上命令,加载了新的内核模块,同时创建了两个新的字符设备,分别是 /dev/chrdev1 和 /dev/chrdev2,开始进行读写测试。

xiechen@xiechen-Ubuntu:~/文档/1.linux/0.内核/2.字符设备驱动多设备支持$ sudo echo "jerome" > sudo /dev/chrdev1
xiechen@xiechen-Ubuntu:~/文档/1.linux/0.内核/2.字符设备驱动多设备支持$ sudo cat /dev/chrdev1 
jerome
xiechen@xiechen-Ubuntu:~/文档/1.linux/0.内核/2.字符设备驱动多设备支持$ sudo echo "maggie" > sudo /dev/chrdev2
xiechen@xiechen-Ubuntu:~/文档/1.linux/0.内核/2.字符设备驱动多设备支持$ sudo cat /dev/chrdev2
maggie

七、使用 cdev 来实现多设备支持

7.1 定义 struct chr_dev

/* 虚拟字符设备;*/
struct chr_dev {
    struct cdev dev;
    char vbuf[BUFF_SIZE];
};

static struct chr_dev vcdev1;
static struct chr_dev vcdev2;

定义一个新的结构体 struct chr_dev,它有两个结构体成员:字符设备结构体 dev 以及设备对应的数据缓冲区。

使用新的结构体类型 struct chr_dev 定义两个虚拟设备 vcdev1 以及 vcdev2。

7.2 设备关联

static int __init chrdev_init(void)
{
    int ret;

    printk("chrdev init\n");
    ret = alloc_chrdev_region(&devno, 0, DEV_CNT,  DEV_NAME);
    if(ret < 0)
        goto alloc_err;

    //关联第一个设备:vdev1;
    cdev_init(&vcdev1.dev, &chr_dev_fops);
    ret = cdev_add(&vcdev1.dev, devno+0, 1);

    if(ret < 0){
        printk("fail to add vcdev1 ");
        goto add_err1;
    }

    // 关联第二个设备:vdev2;
    cdev_init(&vcdev2.dev, &chr_dev_fops);
    ret = cdev_add(&vcdev2.dev, devno+1, 1);

    if(ret < 0){
        printk("fail to add vcdev2 ");
        goto add_err2;
    }
    return 0;

add_err2:
    cdev_del(&(vcdev1.dev));
add_err1:
    unregister_chrdev_region(devno, DEV_CNT);
alloc_err:
    return ret;
}

在添加字符设备时,使用 cdev_add 依次添加。

当虚拟设备 1 添加失败时,直接返回的时候,只需要注销申请到的设备号即可。

若虚拟设备 2 添加失败,则需要把虚拟设备 1 移动,再将申请的设备号注销。

7.2 设备注销

static void __exit chrdev_exit(void)
{
    printk("chrdev exit\n");
    unregister_chrdev_region(devno, DEV_CNT);
    cdev_del(&(vcdev1.dev));
    cdev_del(&(vcdev2.dev));
}

chrdev_exit 函数注销了申请到的设备号,使用 cdev_del 移动两个虚拟设备。

7.3 操作数据缓冲区

Linux 提供了一个宏定义 container_of,该宏可以根据结构体的某个成员的地址,来得到该结构体的地址。

  1. 是代表结构体成员的真实地址;
  2. 结构体的类型;
  3. 结构体成员的名字。

需要通过 inode 的 i_cdev 成员,来得到对应的虚拟设备结构体,并保存到文件指针 filp 的私有数据成员中。

static int chr_dev_open(struct inode *inode, struct file *filp)
{
    printk("open\n");
    filp->private_data = container_of(inode->i_cdev, struct chr_dev, dev);
    return 0;
}

打开虚拟设备 1,那么 inode->i_cdev 便指向了 vcdev1 的成员dev, 利用 container_of 宏,就可以得到 vcdev1 结构体的地址,也就可以操作对应的数据缓冲区了。

7.4 获取文件私有数据

通过文件指针 filp 的成员 private_data 得到相应的虚拟设备。定义 char 类型的指针变量 vbuf,指向对应设备的数据缓冲区。

static ssize_t chr_dev_write(struct file *filp, const char __user * buf, size_t count, loff_t *ppos)
{
    unsigned long p = *ppos;
    int ret;

    // 获取文件的私有数据;
    struct chr_dev *dev = filp->private_data;
    char *vbuf = dev->vbuf;

    int tmp = count ;
    if(p > BUFF_SIZE)
        return 0;
    if(tmp > BUFF_SIZE - p)
        tmp = BUFF_SIZE - p;
    ret = copy_from_user(vbuf, buf, tmp);
    *ppos += tmp;
    return tmp;
}

static ssize_t chr_dev_read(struct file *filp, char __user * buf, size_t count, loff_t *ppos)
{
    unsigned long p = *ppos;
    int ret;
    int tmp = count ;

    // 获取文件的私有数据;
    struct chr_dev *dev = filp->private_data;
    char *vbuf = dev->vbuf;

    if(p >= BUFF_SIZE)
        return 0;
    if(tmp > BUFF_SIZE - p)
        tmp = BUFF_SIZE - p;
    ret = copy_to_user(buf, vbuf+p, tmp);
    *ppos +=tmp;
    return tmp;
}

7.5 完整源码

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

#define DEV_NAME            "JeromeCharDev"
#define DEV_CNT                 (2)
#define BUFF_SIZE               128
static dev_t devno;

struct chr_dev{
    struct cdev dev;
    char vbuf[BUFF_SIZE];
};
static struct chr_dev vcdev1;
static struct chr_dev vcdev2;

static int chr_dev_open(struct inode *inode, struct file *filp);
static int chr_dev_release(struct inode *inode, struct file *filp);
static ssize_t chr_dev_write(struct file *filp, const char __user * buf, size_t count, loff_t *ppos);
static ssize_t chr_dev_read(struct file *filp, char __user * buf, size_t count, loff_t *ppos);

static struct file_operations chr_dev_fops = {
    .owner = THIS_MODULE,
    .open = chr_dev_open,
    .release = chr_dev_release,
    .write = chr_dev_write,
    .read = chr_dev_read,
};
static int chr_dev_open(struct inode *inode, struct file *filp)
{
    printk("open\n");
    filp->private_data = container_of(inode->i_cdev, struct chr_dev, dev);
    return 0;
}

static int chr_dev_release(struct inode *inode, struct file *filp)
{
    printk("\nrelease\n");
    return 0;
}

static ssize_t chr_dev_write(struct file *filp, const char __user * buf, size_t count, loff_t *ppos)
{
    unsigned long p = *ppos;
    int ret;
    struct chr_dev *dev = filp->private_data;
    char *vbuf = dev->vbuf;

    int tmp = count ;
    if(p > BUFF_SIZE)
        return 0;
    if(tmp > BUFF_SIZE - p)
        tmp = BUFF_SIZE - p;
    ret = copy_from_user(vbuf, buf, tmp);
    *ppos += tmp;
    return tmp;
}

static ssize_t chr_dev_read(struct file *filp, char __user * buf, size_t count, loff_t *ppos)
{
    unsigned long p = *ppos;
    int ret;
    int tmp = count ;
    struct chr_dev *dev = filp->private_data;
    char *vbuf = dev->vbuf;
    if(p >= BUFF_SIZE)
        return 0;
    if(tmp > BUFF_SIZE - p)
        tmp = BUFF_SIZE - p;
    ret = copy_to_user(buf, vbuf+p, tmp);
    *ppos +=tmp;
    return tmp;
}

static int __init chrdev_init(void)
{
    int ret;
    printk("chrdev init\n");
    ret = alloc_chrdev_region(&devno, 0, DEV_CNT,  DEV_NAME);
    if(ret < 0)
        goto alloc_err;

    cdev_init(&vcdev1.dev, &chr_dev_fops);
    ret = cdev_add(&vcdev1.dev, devno+0, 1);
    if(ret < 0){
        printk("fail to add vcdev1 ");
        goto add_err1;
    }
    cdev_init(&vcdev2.dev, &chr_dev_fops);
    ret = cdev_add(&vcdev2.dev, devno+1, 1);
    if(ret < 0){
        printk("fail to add vcdev2 ");
        goto add_err2;
    }
    return 0;
add_err2:
    cdev_del(&(vcdev1.dev));
add_err1:
    unregister_chrdev_region(devno, DEV_CNT);
alloc_err:
    return ret;
}

module_init(chrdev_init);

static void __exit chrdev_exit(void)
{
    printk("chrdev exit\n");
    unregister_chrdev_region(devno, DEV_CNT);
    cdev_del(&(vcdev1.dev));
    cdev_del(&(vcdev2.dev));
}
module_exit(chrdev_exit);

MODULE_LICENSE("GPL");

7.6 echo / cat 测试

xiechen@xiechen-Ubuntu:~/文档/1.linux/0.内核/3.通过cdev对字符设备驱动多设备支持$ tree
.
├── jeromeDev.c
└── Makefile

0 directories, 2 files
xiechen@xiechen-Ubuntu:~/文档/1.linux/0.内核/3.通过cdev对字符设备驱动多设备支持$ make
make -C /lib/modules/5.4.0-42-generic/build   M=/home/xiechen/文档/1.linux/0.内核/3.通过cdev对字符设备驱动多设备支持 modules   # 编译内核模块。-C 将工作目录转到 KERNELBUILD,调用该目录下的 Makefile,并向这个 Makefile 传递参数 M 的值是 /home/xiechen/文档/1.linux/0.内核/3.通过cdev对字符设备驱动多设备支持 modules;
make[1]: 进入目录“/usr/src/linux-headers-5.4.0-42-generic”
  CC [M]  /home/xiechen/文档/1.linux/0.内核/3.通过cdev对字符设备驱动多设备支持/jeromeDev.o
  Building modules, stage 2.
  MODPOST 1 modules
  CC [M]  /home/xiechen/文档/1.linux/0.内核/3.通过cdev对字符设备驱动多设备支持/jeromeDev.mod.o
  LD [M]  /home/xiechen/文档/1.linux/0.内核/3.通过cdev对字符设备驱动多设备支持/jeromeDev.ko
make[1]: 离开目录“/usr/src/linux-headers-5.4.0-42-generic”
xiechen@xiechen-Ubuntu:~/文档/1.linux/0.内核/3.通过cdev对字符设备驱动多设备支持$ sudo insmod jeromeDev.ko
xiechen@xiechen-Ubuntu:~/文档/1.linux/0.内核/3.通过cdev对字符设备驱动多设备支持$ sudo mknod /dev/chrdev1 c 240 0
xiechen@xiechen-Ubuntu:~/文档/1.linux/0.内核/3.通过cdev对字符设备驱动多设备支持$ sudo mknod /dev/chrdev2 c 240 1
xiechen@xiechen-Ubuntu:~/文档/1.linux/0.内核/3.通过cdev对字符设备驱动多设备支持$ sudo echo "jerome" > sudo /dev/chrdev1 
xiechen@xiechen-Ubuntu:~/文档/1.linux/0.内核/3.通过cdev对字符设备驱动多设备支持$ sudo echo "maggie" > sudo /dev/chrdev2 
xiechen@xiechen-Ubuntu:~/文档/1.linux/0.内核/3.通过cdev对字符设备驱动多设备支持$ sudo cat /dev/chrdev1
jerome
xiechen@xiechen-Ubuntu:~/文档/1.linux/0.内核/3.通过cdev对字符设备驱动多设备支持$ sudo cat /dev/chrdev2
maggie

八、总结

一个驱动支持多个设备的具体实现方式的重点在于如何运用 file 的私有数据成员。

第一种方法是通过将各自的数据缓冲区放到该成员中,在读写函数的时候,直接就可以对相应的数据缓冲区进行操作;

第二种方法则是通过将我们的数据缓冲区和字符设备结构体封装到一起,由于文件结构体 inode 的成员 i_cdev 保存了对应字符设备结构体,使用 container_of 宏便可以获得封装后的结构体的地址,进而得到相应的数据缓冲区。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

解琛

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值