关注微信公众号:Linux内核拾遗
前文已经介绍了系统中断的基础知识,本文立刻马上进入系统中断的编程实战。
1 中断编程特点
和普通的内核编程不同,中断代码的编写有如下的特点(或者需要注意的地方):
- **避免睡眠:**中断处理程序不能调用会导致睡眠的函数。
- **使用自旋锁:**进入临界区时,用自旋锁代替互斥锁,因为互斥锁会导致睡眠。
- **禁止与用户空间交换数据:**中断处理程序不应直接与用户空间交互。
- **快速处理并延迟工作:**中断处理程序应快速执行,复杂任务应放在下半部分,通过软中断(softirq)、任务队列(tasklet)或工作队列(workqueue)处理。
- **防止重入:**确保处理中断时禁用相应的IRQ,防止重复调用。
- **合理设置优先级:**中断处理程序可以被更高优先级的中断打断,必要时标记为快速处理,但过多会影响性能。
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中:
- IRQF_DISABLED:指示内核在执行中断处理程序时禁用所有中断,大部分情况下不应该设置此flag,必须时候保留给执行速度快且性能敏感的中断。
- IRQF_SAMPLE_RANDOM:指定由此设备生成的中断应贡献到内核熵池,其中内核熵池提供由各种随机事件派生的真正随机数。不要在设备以可预测的速率发出中断(例如系统计时器)或可能被外部攻击者影响(例如网络设备)时设置此标志。
- IRQF_SHARED:指定中断线可以在多个中断处理程序之间共享,否则每条中断线上只能存在一个处理程序。
- 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 其他函数
- enable_irq:启用指定的中断线,允许相应的中断请求(IRQ)触发中断处理程序。
- disable_irq:禁用指定的中断线,阻止该中断线触发中断处理程序。此函数等待所有正在处理的中断处理程序完成后再返回。
- disable_irq_nosync:禁用指定的中断线,但不等待当前正在处理的中断处理程序完成。这可以提高性能,但需要确保不会导致竞态条件。
- in_irq:用于确定代码是否在硬中断处理程序(Interrupt handler)中运行。
- 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头文件中:
其中外部中断源可用的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内核拾遗