实用指南:如何编写您的第一个 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 
#include 
#include 
#include 
#include 
#include 

初始化


现在我们看一下设备初始化函数。

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

完整代码


我欢迎建设性的批评,并期待您的反馈。

如果您发现任何错误,或者我的材料呈现方式不正确,请告知我。
为了更快地收到回复,请给我发送私信。

谢谢!

---

技能提升

如果你觉得本文有帮助,以下资源可以帮你深入学习:

  1. 趣谈Linux操作系统轻松理解Linux操作系统核心原理

部署资源

posted @ 2025-11-13 14:20  clnchanpin  阅读(32)  评论(0)    收藏  举报