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

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

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

#include 
#include 
#include 
#include 

/* 定义一个字符设备对象 */
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 结构的定义在内核源码文件 中,如下图所示:

Linux设备驱动-字符设备驱动浅析_第1张图片

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

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

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



#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,其定义如下(添加了注释说明):



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(),该函数不仅分配内存空间,还会对其进行必要的初始化,如下代码:



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(),其源码如下:



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(), 其源代码如下:



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() 函数代码如下:



/* 结构 */
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书籍资料。
Linux设备驱动-字符设备驱动浅析_第2张图片

你可能感兴趣的:(Linux驱动,linux,驱动开发)