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
中的函数指针成员而展开的。
应用程序通过对文件系统提供的 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
结构体记录了加入系统的字符设备的有关信息,重点关注的内容:
总体来说,设备驱动程序通过调用 cdev_add()
函数,把它所管理的设备对象的指针添加到类型为 struct probe
的节点中,然后再把该节点加入到 cdev_map
实现的哈希链表中。
设备驱动程序调用 cdev_add()
成功之后,意味着是一个字符设备对象已经加入到了系统,可以被系统调用。用户程序可以通过文件系统的接口调用,转接调用这个驱动程序。
本篇文章主要介绍了字符设备驱动程序涉及到关键数据结构,以及字符设备注册到系统的具体实现。
对于字符设备驱动程序来说,核心工作是实现 struct file_operations
对象中各类函数,此结构中虽然定义了众多的函数指针,实际上设备驱动程序并不需要为每一个函数指针提供具体的实现。