Linux设备驱动系列(13) —— 系统中断编程

166 阅读6分钟

关注微信公众号:Linux内核拾遗

文章来源:mp.weixin.qq.com/s/K_I_zKtvd…

前文已经介绍了系统中断的基础知识,本文立刻马上进入系统中断的编程实战。

Linux设备驱动系列(12) —— 系统中断概述

1 中断编程特点

和普通的内核编程不同,中断代码的编写有如下的特点(或者需要注意的地方):

  1. **避免睡眠:**中断处理程序不能调用会导致睡眠的函数。
  2. **使用自旋锁:**进入临界区时,用自旋锁代替互斥锁,因为互斥锁会导致睡眠。
  3. **禁止与用户空间交换数据:**中断处理程序不应直接与用户空间交互。
  4. **快速处理并延迟工作:**中断处理程序应快速执行,复杂任务应放在下半部分,通过软中断(softirq)、任务队列(tasklet)或工作队列(workqueue)处理。
  5. **防止重入:**确保处理中断时禁用相应的IRQ,防止重复调用。
  6. **合理设置优先级:**中断处理程序可以被更高优先级的中断打断,必要时标记为快速处理,但过多会影响性能。

2 中断相关函数

下面介绍一下中断相关的基本内核函数。

2.1 request_irq

函数request_irq()用于向内核中注册一个中断处理函数:

request_irq(unsigned int irq, irq_handler_t handler,
            unsigned long flags, const char *name, void *dev_id);

当内核接收到中断号为irq的中断时就会调用handler函数进行处理。

flags的相关定义在头文件linux/interrupt.h中:

  1. IRQF_DISABLED:指示内核在执行中断处理程序时禁用所有中断,大部分情况下不应该设置此flag,必须时候保留给执行速度快且性能敏感的中断。
  2. IRQF_SAMPLE_RANDOM:指定由此设备生成的中断应贡献到内核熵池,其中内核熵池提供由各种随机事件派生的真正随机数。不要在设备以可预测的速率发出中断(例如系统计时器)或可能被外部攻击者影响(例如网络设备)时设置此标志。
  3. IRQF_SHARED:指定中断线可以在多个中断处理程序之间共享,否则每条中断线上只能存在一个处理程序。
  4. IRQF_TIMER:指定此处理程序处理系统计时器的中断。

name指定的是使用该IRQ的设备名称,它会显示在/proc/interrupts中。

dev_id:如果中断线未共享,可以传递 NULL;如果中断线是共享的,必须传递一个唯一标识符,用于在共享中断线上唯一标识中断处理程序,确保内核能够正确移除特定的处理程序。

需要注意的是,request_irq()不能在中断上下文(或者其他不应该阻塞的代码)中调用,因为该函数调用会导致阻塞。

2.2 free_irq

函数free_irq()用于释放由request_irq()注册的中断处理函数:

free_irq(unsigned int irq, void *dev_id);

如果指定的中断线未共享,此函数会移除处理程序并禁用该中断线。如果中断线是共享的,通过 dev_id 标识的处理程序会被移除,但只有在最后一个处理程序被移除时才会禁用中断线。

在共享中断线的情况下,需要一个唯一的标识符来区分同一条线上可能存在的多个处理程序,并使 free_irq() 能够只移除正确的处理程序。无论中断线是否共享,如果 dev_id 非空,它必须与所需的处理程序匹配。

调用 free_irq() 必须在进程上下文中进行。

2.3 其他函数

  1. enable_irq:启用指定的中断线,允许相应的中断请求(IRQ)触发中断处理程序。
  2. disable_irq:禁用指定的中断线,阻止该中断线触发中断处理程序。此函数等待所有正在处理的中断处理程序完成后再返回。
  3. disable_irq_nosync:禁用指定的中断线,但不等待当前正在处理的中断处理程序完成。这可以提高性能,但需要确保不会导致竞态条件。
  4. in_irq:用于确定代码是否在硬中断处理程序(Interrupt handler)中运行。
  5. in_interrupt:检查当前是否在硬中断(Interrupt handler)或者软中断处理程序(Bottom half)中运行。

2.4 使用示例

// 1. 注册中断处理函数
#define IRQ_NO 11
if (request_irq(IRQ_NO, irq_handler, IRQF_SHARED, "my_device", (void *)(irq_handler))) {
	pr_info("my_device: cannot register IRQ ");
	return -EINVAL;
}

// 2. 释放中断处理函数
free_irq(IRQ_NO,(void *)(irq_handler));

// 3. 中断处理函数代码
static irqreturn_t irq_handler(int irq,void *dev_id) {
	pr_info("Shared IRQ: Interrupt Occurred");
	return IRQ_HANDLED;
}

3 中断模拟

下面介绍一下如何通过软件方式来模拟触发硬件中断。

Intel 处理器使用中断描述符表 (IDT) 来处理中断。IDT 由 256 个条目组成,每个条目对应一个向量,并且每个条目占 8 个字节。所有条目都是指向中断处理函数的指针。IDT的地址存储在 IDTR(IDT 寄存器)中。它们之间的关系可以如下图所示:

代码中可以通过指令"int"来触发一个中断,例如"int $0x80"将触发一个Linux系统调用。

Linux内核的中断向量表映射关系定义在arch/x86/include/asm/irq_vectors.h头文件中:

image-20240526140847538

其中外部中断源可用的IDT向量是从0x20开始的,并且0x80分配给系统调用向量,0x30~0x3f预留给ISA。

/*
 * IDT vectors usable for external interrupt sources start at 0x20.
 * (0x80 is the syscall vector, 0x30-0x3f are for ISA)
 */
#define FIRST_EXTERNAL_VECTOR		0x20

#define IA32_SYSCALL_VECTOR		0x80

/*
 * Vectors 0x30-0x3f are used for ISA interrupts.
 *   round up to the next 16-vector boundary
 */
#define ISA_IRQ_VECTOR(irq)		(((FIRST_EXTERNAL_VECTOR + 16) & ~15) + irq)

因此执行asm("int $0x3B")将会往IRQ 11发起中断。

4 完整示例代码

kernel_driver.c

#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kdev_t.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/slab.h>
#include <linux/uaccess.h>
#include <linux/sysfs.h>
#include <linux/kobject.h>
#include <linux/interrupt.h>
#include <asm/io.h>
#include <linux/err.h>

#define IRQ_NO 12
static irqreturn_t irq_handler(int irq, void *dev_id)
{
	pr_info("Shared IRQ[%d]: Interrupt Occurred\n", irq);
	return IRQ_HANDLED;
}
volatile int my_value = 0;

dev_t dev = 0;
static struct class *dev_class;
static struct cdev my_cdev;
struct kobject *kobj_ref;

static ssize_t sysfs_show(struct kobject *kobj,
						  struct kobj_attribute *attr, char *buf)
{
	return sprintf(buf, "%d", my_value);
}
static ssize_t sysfs_store(struct kobject *kobj,
						   struct kobj_attribute *attr, const char *buf, size_t count)
{
	sscanf(buf, "%d", &my_value);
	return count;
}

struct kobj_attribute my_attr = __ATTR(my_value, 0660, sysfs_show, sysfs_store);

static ssize_t my_read(struct file *filp,
					   char __user *buf, size_t len, loff_t *off)
{
	asm("int $0x3C");
	return 0;
}

static struct file_operations fops = {
	.owner = THIS_MODULE,
	.read = my_read,
};

static int __init my_driver_init(void)
{
	if ((alloc_chrdev_region(&dev, 0, 1, "my_dev")) < 0)
		return -1;

	cdev_init(&my_cdev, &fops);
	my_cdev.owner = THIS_MODULE;
	my_cdev.ops = &fops;

	if ((cdev_add(&my_cdev, dev, 1)) < 0)
		goto r_class;

	if (IS_ERR(dev_class = class_create(THIS_MODULE, "my_class")))
		goto r_class;

	if (IS_ERR(device_create(dev_class, NULL, dev, NULL, "my_device")))
		goto r_device;

	kobj_ref = kobject_create_and_add("my_sysfs", kernel_kobj);
	if (sysfs_create_file(kobj_ref, &my_attr.attr))
		goto r_sysfs;

	if (request_irq(IRQ_NO, irq_handler, IRQF_SHARED, "my_device", (void *)(irq_handler)))
		goto irq;

	return 0;
irq:
	free_irq(IRQ_NO, (void *)(irq_handler));
r_sysfs:
	kobject_put(kobj_ref);
	sysfs_remove_file(kernel_kobj, &my_attr.attr);

r_device:
	class_destroy(dev_class);
r_class:
	unregister_chrdev_region(dev, 1);
	cdev_del(&my_cdev);
	return -1;
}

static void __exit my_driver_exit(void)
{
	free_irq(IRQ_NO, (void *)(irq_handler));
	kobject_put(kobj_ref);
	sysfs_remove_file(kernel_kobj, &my_attr.attr);
	device_destroy(dev_class, dev);
	class_destroy(dev_class);
	cdev_del(&my_cdev);
	unregister_chrdev_region(dev, 1);
}

module_init(my_driver_init);
module_exit(my_driver_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("feifei <feifei@gmail.com>");
MODULE_DESCRIPTION("A simple device driver");
MODULE_VERSION("1.9");

关注微信公众号:Linux内核拾遗

文章来源:mp.weixin.qq.com/s/K_I_zKtvd…