一文秒懂|Linux字符设备驱动

1、前言

众所周知,Linux内核主要包括三种驱动模型,字符设备驱动,块设备驱动以及网络设备驱动。

其中,Linux字符设备驱动,可以说是Linux驱动开发中最常见的一种驱动模型。

我们该系列文章,主要为了帮助大家快速入门Linux驱动开发,该篇主要来了解一些字符设备驱动的框架和机制。

系列文章基于Kernel 4.19

2、关键数据结构

2.1 cdev

复制代码
 struct cdev {
     struct kobject kobj;

             struct module *owner;

                  const struct file_operations *ops;

                        struct list_head list;

                            dev_t dev;

                               unsigned int count;

                               } __randomize_layout;

结构体名称cdev

文件位置include/linux/cdev.h

主要作用cdev可以理解为char device,用来抽象一个字符设备。

核心成员及含义

  • kobj:表示一个内核对象。

  • owner:指向该模块的指针

  • ops:指向文件操作的指针,包括openreadwrite等操作接口

  • list:用于将该设备加入到内核模块链表中

  • dev:设备号,由主设备号和次设备号构成

  • count:表示有多少个同类型设备,也间接表示设备号的范围

  • __randomize_layout:一个编译器指令,用于随机化结构体的布局,以增加安全性。

2.2 file_operations

复制代码
 struct file_operations {
     struct module *owner;

              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 *);

                                   ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);

                                   ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);

                                   int (*iterate) (struct file *, struct dir_context *);

                                   int (*iterate_shared) (struct file *, struct dir_context *);

                                   __poll_t (*poll) (struct file *, struct poll_table_struct *);

                                   long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);

                                   long (*compat_ioctl) (struct file *, unsigned int, unsigned long);

                                   int (*mmap) (struct file *, struct vm_area_struct *);

                                   unsigned long mmap_supported_flags;

                                   int (*open) (struct inode *, struct file *);

                                   int (*flush) (struct file *, fl_owner_t id);

                                   int (*release) (struct inode *, struct file *);

                                   int (*fsync) (struct file *, loff_t, loff_t, int datasync);

                                   int (*fasync) (int, struct file *, int);

                                   int (*lock) (struct file *, int, struct file_lock *);

                                   ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);

                                   unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);

                                   int (*check_flags)(int);

                                   int (*flock) (struct file *, int, struct file_lock *);

                                   ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);

                                   ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);

                                   int (*setlease)(struct file *, long, struct file_lock **, void **);

                                   long (*fallocate)(struct file *file, int mode, loff_t offset,

                                             loff_t len);

                                   void (*show_fdinfo)(struct seq_file *m, struct file *f);

                               #ifndef CONFIG_MMU

                                   unsigned (*mmap_capabilities)(struct file *);

                               #endif

                                   ssize_t (*copy_file_range)(struct file *, loff_t, struct file *,

                                           loff_t, size_t, unsigned int);

                                   int (*clone_file_range)(struct file *, loff_t, struct file *, loff_t,

                                           u64);

                                   int (*dedupe_file_range)(struct file *, loff_t, struct file *, loff_t,

                                           u64);

                                   int (*fadvise)(struct file *, loff_t, loff_t, int);

                               } __randomize_layout;

                               ​

结构体名称file_operations

文件位置include/linux/fs.h

主要作用 :正如其名,主要用来描述文件操作的各种接口,Linux一切接文件的思想,内核想要操作哪个文件,都需要通过这些接口来实现。

核心成员及含义

  • open:打开文件的函数

  • read:读取文件的函数。

  • write:写入文件的函数。

  • release:关闭文件的函数。

  • flush:刷新文件的函数,通常在关闭文件时调用。

  • llseek:改变文件读写指针位置的函数。

  • fsync:将文件数据同步写入磁盘的函数。

  • poll:询问文件是否可被非阻塞读写

2.3 dev_t

复制代码
 typedef u32 __kernel_dev_t;
 ​

        typedef __kernel_dev_t  dev_t;

类型名称dev_t

文件位置include/linux/types.h

主要作用 :表示字符设备对应的设备号,其中包括主设备号和次设备号。

3、数据结构之间关系

上图绘制是对字符设备驱动程序的数据结构以及API的关系图,

有需要原始文件,可在公~号【嵌入式艺术】获取。

4、字符设备驱动整体架构

4.1 加载与卸载函数

驱动首先实现的就是加载和卸载函数,也是驱动程序的入口函数。

我们一般这么定义驱动的加载卸载函数:

复制代码
 static int __init xxx_init(void)

        {

         ​

           }

            ​

              static void __exit xxx_exit(void)

                    {

                         

                      }

                       ​

                         module_init(xxx_init);

                            module_exit(xxx_exit);

这段代码就是实现一个通用驱动的加载与卸载,关于module_initmodule_exit的实现机制,可以查看之前总结文章。

4.2 设备号管理

4.2.1 设备号的概念

每一类字符设备都有一个唯一的设备号,其中设备号又分为主设备号和次设备号,那么这两个分别作用是什么呢?

  • 主设备号:用于标识设备的类型,

  • 次设备号:用于区分同类型的不同设备

简单来说,主设备号用于区分是IIC设备还是SPI设备,而次设备号用于区分IIC设备下,具体哪一个设备,是MPU6050还是EEPROM

4.2.2 设备号的分配

了解了设备号的概念,Linux中设备号有那么多,那么我们该如何去使用正确的设备号呢?

设备号的分配方式有两种,一种是动态分配,另一种是静态分配,也可以理解为一种是内核自动分配,一种是手动分配。

静态分配函数

复制代码
 int register_chrdev_region(dev_t from, unsigned count, const char *name);
  • from:表示已知的一个设备号

  • count:表示连续设备编号的个数,(同类型的设备有多少个)

  • name:表示设备或者驱动的名称

函数作用 :以from设备号开始,连续分配count个同类型的设备号

动态分配函数

复制代码
 int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);
  • dev:设备号的指针,用于存放分配的设备号的值

  • baseminor:次设备号开始分配的起始值

  • count:表示连续设备编号的个数,(同类型的设备有多少个)

  • name:表示设备或者驱动的名称

函数作用 :从baseminor次设备号开始,连续分配count个同类型的设备号,并自动分配一个主设备号,将主、次组成的设备号信息赋值给*dev

这两个函数最大的区别在于

  • register_chrdev_region:调用前,已预先定义好了主设备号和次设备号,调用该接口后,会将自定义的设备号登记加入子系统中,方便系统追踪系统设备号的使用情况。

  • alloc_chrdev_region:调用前,未定义主设备号和次设备号;调用后,主设备号以0来表示,以自动分配,并且将自动分配的设备号,同样加入到子系统中,方便系统追踪系统设备号的使用情况。

这两个函数的共同点在于

系统维护了一个数组列表,用来登记所有的已使用的设备号信息,这两个接口归根到底也是将其设备号信息,登记到系统维护的设备号列表中,以免后续冲突使用。

Linux中,我们可以通过cat /proc/devices命令,查看所有i登记的设备号列表。

后面有时间,我们可以详细聊设备号的自动分配机制,管理机制。

4.2.3 设备号的注销

设备号作为一种系统资源,当所对应的设备卸载时,当然也要将其所占用的设备号归还给系统,无论时静态分配,还是动态分配,最终都是调用下面函数来注销的。

复制代码
 void unregister_chrdev_region(dev_t from, unsigned count);
  • from:表示已知的一个设备号

  • count:表示连续设备编号的个数,(同类型的设备有多少个)

函数作用 :要注销from主设备号下的连续count个设备

4.2.4 设备号的获取

设备号的管理很简单,在关键数据结构中,我们看到设备号的类型是dev_t,也就是u32类型表示的一个数值。

其中主设备号和次设备号的分界线,由MINORBITS宏定义指定:

复制代码
 #define MINORBITS   20

也就是主设备号占用高12bit,次设备号占用低20bit

并且,内核还提供了相关API接口,来获取主设备号和次设备号,以及生成设备号的接口,如下:

复制代码
#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))

以上,通过移位操作,来实现主次设备号的获取。

4.2.4 通用代码实现

复制代码
#define CUSTOM_DEVICE_NUM 0
#define DEVICE_NUM 1
#device DEVICE_NAME "XXXXXX"
static dev_t global_custom_major = CUSTOM_DEVICE_NUM;

static int __init xxx_init(void)
{
    dev_t custom_device_number= MKDEV(global_custom_major, 0);	//	custom device number
    /* device number register*/
    if (global_custom_major) {
        ret = register_chrdev_region(custom_device_number, DEVICE_NUM, DEVICE_NAME);
    } else {
        ret = alloc_chrdev_region(&custom_device_number, 0, DEVICE_NUM, DEVICE_NAME);
        global_custom_major = MAJOR(custom_device_number);
    }
}

static void __exit xxx_exit(void)
{
    unregister_chrdev_region(MKDEV(global_mem_major, 0), DEVICE_NUM);
}

module_init(xxx_init);
module_exit(xxx_exit);

该函数实现了设备号的分配,如果主设备号为0,则采用动态配分的方式,否则采用静态分配的方式。

更多干货可见:高级工程师聚集地,助力大家更上一层楼!

4.3 字符设备的管理

了解完设备号的管理之后,我们来看下字符设备是如何管理的。

4.3.1、字符设备初始化

复制代码
void cdev_init(struct cdev *cdev, const struct file_operations *fops);
  • cdev:一个字符设备对象,也就是我们创建好的字符设备

  • fops:该字符设备的文件处理接口

函数作用 :初始化一个字符设备,并且将所对应的文件处理指针与字符设备绑定起来。

4.3.2、字符设备注册

复制代码
int cdev_add(struct cdev *p, dev_t dev, unsigned count);
  • p:一个字符设备指针,只想待添加的字符设备对象

  • dev:该字符设备所负责的第一个设备编号

  • count:该类型设备的个数

函数作用 :添加一个字符设备驱动到Linux系统中。

4.3.3、字符设备注销

复制代码
void cdev_del(struct cdev *p);
  • p:指向字符设备对象的指针

函数作用 :从系统中移除该字符设备驱动

4.4 文件操作接口的实现

因为在Linux中,一切皆文件的思想,所以每一个字符设备,也都有一个文件节点来对应。

我们在初始化字符设备的时候,会将struct file_operations的对象与字符设备进行绑定,其作用是来处理该字符设备的openreadwrite等操作。

我们要做的就是去实现我们需要的函数接口,如:

复制代码
static const struct file_operations global_mem_fops = {
    .owner = THIS_MODULE,
    .llseek = global_mem_llseek,
    .read = global_mem_read,
    .write = global_mem_write,
    .unlocked_ioctl = global_mem_ioctl,
    .open = global_mem_open,
    .release = global_mem_release,
};

至此,我们一个基本的字符设备驱动程序的框架就基本了然于胸了

5、总结

本篇文章,旨在通俗易懂的讲解:

  • 字符设备驱动相关数据结构

  • 数据结构关系图

  • 核心API接口

  • 字符设备驱动整体框架

希望对大家有所帮助。

相关推荐
C++忠实粉丝3 分钟前
计算机网络socket编程(3)_UDP网络编程实现简单聊天室
linux·网络·c++·网络协议·计算机网络·udp
量子网络22 分钟前
debian 如何进入root
linux·服务器·debian
我们的五年29 分钟前
【Linux课程学习】:进程描述---PCB(Process Control Block)
linux·运维·c++
我言秋日胜春朝★1 小时前
【Linux】进程地址空间
linux·运维·服务器
C-cat.2 小时前
Linux|环境变量
linux·运维·服务器
yunfanleo2 小时前
docker run m3e 配置网络,自动重启,GPU等 配置渠道要点
linux·运维·docker
糖豆豆今天也要努力鸭2 小时前
torch.__version__的torch版本和conda list的torch版本不一致
linux·pytorch·python·深度学习·conda·torch
烦躁的大鼻嘎3 小时前
【Linux】深入理解GCC/G++编译流程及库文件管理
linux·运维·服务器
ac.char3 小时前
在 Ubuntu 上安装 Yarn 环境
linux·运维·服务器·ubuntu
敲上瘾3 小时前
操作系统的理解
linux·运维·服务器·c++·大模型·操作系统·aigc