暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

Linux设备驱动-字符设备驱动浅析

txp玩Linux 2022-01-25
175

点击上方蓝字【一起学嵌入式】关注,一起学习,一起成长

Linux 设备驱动分为三种:字符设备驱动、块设备驱动、网络设备驱动。内核针对每一类设备都提供了对应的驱动模型框架,包括基本的内核设施和文件系统接口。

其中,字符设备驱动程序是最常见的,也是相对比较容易理解的一种。其典型的程序框架示例,如下:

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>

/* 定义一个字符设备对象 */
static struct cdev chr_dev;
/* 字符设备节点的设备号 */
static dev_t ndev;

static int chr_open(struct inode *nd, struct file *filp)
{
 int major = MAJOR(nd->i-rdev);
 int minor = MINOR(nd->i_rdev);
 printk("chr_open, major=%d, minor=%d\n", major, minor);
 return 0;
}

static ssize_t chr_read(struct file *f, char __user *u, size_t sz, loff_t *off)
{
 printk("chr_read\n");
 return;
}

struct file_operations chr_ops = 
{
 .owner = THIS_MODULE,
 .open = chr_open,
 .read = chr_read,
}

static int demo_init(void)
{
 int ret;
 /* 初始化字符设备对象 */
 cdev_init(&chr_dev, &chr_ops);
 /* 分配注册字符设备号 */
 ret = alloc_chrdev_region(&ndev, 0, 1, "chr_dev");
 if(ret < 0)
 {
  return ret;
 }
 /* 注册字符设备 chr_dev 到内核系统 */
 ret = cdev_add(&chr_dev, ndev, 1);
 if(ret < 0)
 {
  return ret;
 }
 return 0;
}

static int demo_exit(void)
{
 /* 注销设备驱动 chr_dev */
 cdev_del(&chr_dev);
 /* 释放分配的设备号 */
 unregister_chrdev_region(ndev, 1);
}

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

MODULE_LICENSE("GPL");
MODULE_AUTHOR("YiQiXueQianRuShi");
MODULE_DESCRIPTION("A char device demo");

以上代码,展示了一个字符设备驱动程序的典型框架结构,包含了字符设备驱动程序绝大多数的关键因素。

接下来,探讨一下字符设备程序相关的内容。探讨内容涉及到内核版本,参考自  linux-5.15.4。

struct file_operations 结构

struct file_operations 结构的定义在内核源码文件 <include/linux/fs.h>
中,如下图所示:


该结构成员变量几乎全是函数指针。字符设备驱动程序的编写,基本上围绕着如何实现 struct file_operations
中的函数指针成员而展开的。

应用程序通过对文件系统提供的 API 操作,最终会被内核转接到 struct file_operations
中对应函数指针的具体实现上。

该结构中唯一非函数指针的成员 owner,表示当前 struct file_operations
对象所属的内核模块,几乎都会用 THIS_MODULE
宏给其赋值。宏定义为:

<include/linux/export.h>

#ifdef MODULE
  extern struct module __this_module;
  #define THIS_MODULE (&__this_module)
#else
 #define THIS_MODULE ((struct module *)0)
#endif

__this_module
是内核模块的编译工具链为当前模块产生的 struct file_operations
类型对象。实际上是当前内核模块对象的指针。

如果一个设备文件驱动程序被编译进内核,不是以模块的形式存在,则 THIS_MODULE
被赋值为空指针,无任何作用。

字符设备的数据结构

内核为字符设备抽象出了一个具体的数据结构 struct cdev
,其定义如下(添加了注释说明):

<include/linux/cdev.h>

struct cdev
{
 /* 内嵌的内核对象 */
 struct kobject kobj;
 /* 字符设备驱动程序所在的内核模块对象指针 */
 struct module *owner;
 /* 指向结构 struct file_operations 的指针 */
 const struct file_operations *ops;
 /* 用来将系统中字符设备构成链表 */
 struct list_head list;
 /* 字符设备的设备号 */
 dev_t dev;
 /* 当前设备驱动程序控制的同类设备的数量 */
 unsigned int count;
} __randomize_layout;

分配 struct cdev
对象

可以用两种方式产生 struct cdev
对象:

  • 静态定义的方式。
  • 动态分配的方式。

文章开头部分的示例程序采用的就是静态定义的方式:

static struct cdev chr_dev;

动态分配的方式:

struct cdev *p_chr_dev = kmalloc(sizeof(struct cdev), GFP_KERNEL);

内核源码中提供了一个动态分配 struct cdev
对象的函数 cdev_alloc()
,该函数不仅分配内存空间,还会对其进行必要的初始化,如下代码:

<fs/char_dev.c>

struct cdev *cdev_alloc(void)
{
 /* 申请内存空间 */
 struct cdev *p = kzalloc(sizeof(struct cdev), GFP_KERNEL);
 if (p) {
  INIT_LIST_HEAD(&p->list);
  kobject_init(&p->kobj, &ktype_cdev_dynamic);
 }
 return p;
}

struct cdev
对象分配完成了,接着来看看如何对其进行初始化。

初始化 cdev 对象

内核提供了一个函数,用来对 struct cdev
结构进行初始化,这个函数为 cdev_init()
,其源码如下:

<fs/char_dev.c>

void cdev_init(struct cdev *cdev, const struct file_operations *fops)
{
 memset(cdev, 0, sizeof *cdev);
 INIT_LIST_HEAD(&cdev->list);
 kobject_init(&cdev->kobj, &ktype_cdev_default);
 cdev->ops = fops;
}

函数代码首先对结构各个结构成员赋值为 0,然后配置几个关键成员,重点是把文件操作结构指针赋值给 ops 成员。

到这字符设备驱动程序的一些关键结构介绍完毕。其中的有关设备号的管理可以参考之前的文章:

Linux设备驱动-内核如何管理设备号

接下来看看如何将一个字符设备加入到系统中。

字符设备加入系统

字符设备加入到系统中,也就是字符设备注册。字符设备初始化完成之后,就可以把它加入到系统中,这样别的模块程序就可以使用它。

Linux 提供了把一个字符设备注册到系统中的函数 cdev_add()
, 其源代码如下:

<fs/char_dev.c>

int cdev_add(struct cdev *p, dev_t dev, unsigned count)
{
 int error;

 p->dev = dev;
 p->count = count;

 if (WARN_ON(dev == WHITEOUT_DEV))
  return -EBUSY;

 error = kobj_map(cdev_map, dev, count, NULL, exact_match, exact_lock, p);
 if (error)
  return error;

 kobject_get(p->kobj.parent);

 return 0;
}

参数 p 为要注册进系统的字符设备对象的指针;dev 为该设备的设备号,count 表示设备数量。

cdev_add()
的主要功能是通过 kobj_map()
完成的,kobj_map()
通过操作一个全局变量 cdev_map
,把设备 (*p)加入到其中的哈希链表中。

static struct kobj_map *cdev_map;

变量 cdv_map
的类型为 struct kobj_map
结构指针,在 Linux 系统启动期间由 chrdev_init()
初始化。

struct kobj_map
结构以及 kobj_map()
函数代码如下:

<drivers/base/map.c>

/* 结构 */
struct kobj_map {
 struct probe {
  struct probe *next;
  dev_t dev;
  unsigned long range;
  struct module *owner;
  kobj_probe_t *get;
  int (*lock)(dev_t, void *);
  void *data;
 } *probes[255];
 struct mutex *lock;
};

/* 初始化函数 */
int kobj_map(struct kobj_map *domain, dev_t dev, unsigned long range,
      struct module *module, kobj_probe_t *probe,
      int (*lock)(dev_t, void *), void *data)
{
 unsigned int n = MAJOR(dev + range - 1) - MAJOR(dev) + 1;
 unsigned int index = MAJOR(dev);
 unsigned int i;
 struct probe *p;

 if (n > 255)
  n = 255;

 p = kmalloc_array(n, sizeof(struct probe), GFP_KERNEL);
 if (p == NULL)
  return -ENOMEM;

 for (i = 0; i < n; i++, p++) {
  p->owner = module;
  p->get = probe;
  p->lock = lock;
  p->dev = dev;
  p->range = range;
  p->data = data;
 }
 mutex_lock(domain->lock);
 for (i = 0, p -= n; i < n; i++, p++, index++) {
  struct probe **s = &domain->probes[index % 255];
  while (*s && (*s)->range < range)
   s = &(*s)->next;
  p->next = *s;
  *s = p;
 }
 mutex_unlock(domain->lock);
 return 0;
}

kobj_map()
函数的简单来说就是,通过设备的主设备号 index 来获得 probes 数组的索引值(index % 255
),然后把一个类型为 struct probe
 的节点对象添加到 probes[i]
所管理的链表中。

struct probe
结构体记录了加入系统的字符设备的有关信息,重点关注的内容:

  • 成员 dev 是字符设备的设备号;
  • range 是设备数量;
  • data 存储当前要加入系统的设备对象指针 p;

总体来说,设备驱动程序通过调用 cdev_add()
函数,把它所管理的设备对象的指针添加到类型为 struct probe
的节点中,然后再把该节点加入到 cdev_map
实现的哈希链表中。

设备驱动程序调用 cdev_add()
成功之后,意味着是一个字符设备对象已经加入到了系统,可以被系统调用。用户程序可以通过文件系统的接口调用,转接调用这个驱动程序。

小结

本篇文章主要介绍了字符设备驱动程序涉及到关键数据结构,以及字符设备注册到系统的具体实现。

对于字符设备驱动程序来说,核心工作是实现 struct file_operations
对象中各类函数,此结构中虽然定义了众多的函数指针,实际上设备驱动程序并不需要为每一个函数指针提供具体的实现。



关注我【一起学嵌入式】,一起学习,一起成长。

后台回复 “linux”,获取经典linux书籍资料。


觉得文章不错,点击“分享”、“”、“在看” 呗!

文章转载自txp玩Linux,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论