如何编写您的第一个 Linux 设备驱动程序(一)
大家好!我是大聪明-PLUS!
本文旨在以一个简单的字符驱动程序为例,演示 Linux 系统中设备驱动程序的实现。
对我来说,主要目标是总结并为未来编写内核模块奠定基础,并积累向公众展示技术文献的经验。
附言:
由于篇幅过长,我决定将文章分为三部分:
第一部分 - 内核模块简介、初始化和清除。
第二部分 - open、read、write 和 trim 函数。
第三部分 - 编写 Makefile 并测试设备。
在开始之前,我想先说明一下,本节将介绍基础知识;更详细的信息将在本文的第二部分和最后一部分提供。
那么,让我们开始吧。
准备工作
字符驱动程序是指与字符设备配合使用的驱动程序。
字符设备是指可以作为字节流访问的设备。
例如 /dev/ttyS0 和 /dev/tty1 就是字符设备。
更新。
关于内核版本:
~$ uname -r
4.4.0-93-generic
驱动程序用 scull_dev 结构表示每个字符设备,并且还向内核提供 cdev 接口。
struct scull_dev {struct scull_qset *data; int quantum; int qset; unsigned long size; struct semaphore sem; struct cdev cdev;
};struct scull_dev *scull_device;
该设备将是一个指针链接列表,每个指针指向一个 scull_qset 结构。
struct scull_qset {void **data;struct scull_qset *next;
};
为了更清晰地理解,请看图。
要注册设备,您需要指定一些特殊的编号:
MAJOR – 主设备号(在系统中唯一);
MINOR – 次设备号(在系统中不唯一)。
内核提供了一种机制,允许您手动注册专用编号,但这种方法并不可取;最好礼貌地请求内核为您动态分配这些编号。示例代码如下。
定义好设备的编号后,我们需要将这些编号与驱动程序操作关联起来。这可以使用 file_operations 结构体来完成。
struct file_operations scull_fops = {.owner = THIS_MODULE,.read = scull_read,.write = scull_write,.open = scull_open,.release = scull_release,
};
内核包含特殊的宏 module_init/module_exit,它们指定模块初始化/删除函数的路径。如果没有这些定义,初始化/删除函数将永远不会被调用。
module_init(scull_init_module);
module_exit(scull_cleanup_module);
我们将在这里存储有关设备的基本信息。
int scull_major = 0;
int scull_minor = 0;
int scull_nr_devs = 1;
int scull_quantum = 4000;
int scull_qset = 1000;
准备工作的最后一步是包含头文件。
下面给出了一个简短的描述。
#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/slab.h>
#include <asm/uaccess.h>
初始化
现在我们看一下设备初始化函数。
static int scull_init_module(void)
{int rv, i;dev_t dev;rv = alloc_chrdev_region(&dev, scull_minor, scull_nr_devs, "scull"); if (rv) {printk(KERN_WARNING "scull: can't get major %d\n", scull_major);return rv;}scull_major = MAJOR(dev);scull_device = kmalloc(scull_nr_devs * sizeof(struct scull_dev), GFP_KERNEL);if (!scull_device) {rv = -ENOMEM;goto fail;}memset(scull_device, 0, scull_nr_devs * sizeof(struct scull_dev));for (i = 0; i < scull_nr_devs; i++) {scull_device[i].quantum = scull_quantum;scull_device[i].qset = scull_qset;sema_init(&scull_device[i].sem, 1);scull_setup_cdev(&scull_device[i], i);}dev = MKDEV(scull_major, scull_minor + scull_nr_devs); return 0;fail:scull_cleanup_module();return rv;
}
首先,通过调用 alloc_chrdev_region ,我们注册了一系列设备符号编号并指定了设备名称。然后,通过调用 MAJOR(dev) ,我们获取主设备号。
接下来,检查返回值;如果返回的是错误代码,则退出该函数。值得注意的是,在开发实际的设备驱动程序时,应该始终检查返回值以及指向任何元素的指针(例如 NULL)。
rv = alloc_chrdev_region(&dev, scull_minor, scull_nr_devs, "scull"); if (rv) {printk(KERN_WARNING "scull: can't get major %d\n", scull_major);return rv;
}scull_major = MAJOR(dev);
如果返回值不是错误代码,我们继续初始化。
我们通过调用 kmalloc 函数分配内存,并确保检查指针是否为 NULL。
更新版
值得一提的是,您无需调用两个函数 kmalloc 和 memset,而是可以使用一个调用 kzalloc,它将分配一个内存区域并将其初始化为零。
scull_device = kmalloc(scull_nr_devs * sizeof(struct scull_dev), GFP_KERNEL);if (!scull_device) {rv = -ENOMEM;goto fail;
}memset(scull_device, 0, scull_nr_devs * sizeof(struct scull_dev));
我们继续初始化。这里的关键函数是 scull_setup_cdev,我们将在下面讨论它。MKDEV 用于存储主设备号和次设备号。
for (i = 0; i < scull_nr_devs; i++) {scull_device[i].quantum = scull_quantum;scull_device[i].qset = scull_qset;sema_init(&scull_device[i].sem, 1);scull_setup_cdev(&scull_device[i], i);}dev = MKDEV(scull_major, scull_minor + scull_nr_devs);
我们返回值或处理错误并删除设备。
return 0;fail:scull_cleanup_module();return rv;
}
上面我们介绍了 scull_dev 和 cdev 结构体,它们实现了设备与内核之间的接口。 scull_setup_cdev 函数初始化该结构体并将其添加到系统中。
static void scull_setup_cdev(struct scull_dev *dev, int index)
{int err, devno = MKDEV(scull_major, scull_minor + index);cdev_init(&dev->cdev, &scull_fops); dev->cdev.owner = THIS_MODULE;dev->cdev.ops = &scull_fops;err = cdev_add(&dev->cdev, devno, 1);if (err)printk(KERN_NOTICE "Error %d adding scull %d", err, index);
}
移动
当设备模块从内核中移除时,会调用 scull_cleanup_module 函数。
该函数反转初始化过程,删除设备结构、释放内存,并移除内核分配的主设备号和次设备号。
void scull_cleanup_module(void)
{int i;dev_t devno = MKDEV(scull_major, scull_minor);if (scull_device) {for (i = 0; i < scull_nr_devs; i++) {scull_trim(scull_device + i);cdev_del(&scull_device[i].cdev); }kfree(scull_device);}unregister_chrdev_region(devno, scull_nr_devs);
}
我欢迎建设性的批评,并期待您的反馈。
如果您发现任何错误,或者我的材料呈现方式不正确,请告知我。
为了更快地收到回复,请给我发送私信。
谢谢!