【Linux】内核中断机制

在这里插入图片描述

博客主页:PannLZ
系列专栏:《Linux系统之路》
欢迎关注:点赞收藏✍️留言

文章目录

    • 内核中断机制
      • 1.注册中断处理函数
      • 2.下半部的概念
        • 1.1问题——中断处理程序的设计限制
        • 1.2解决方案——下半部
        • 1.3Tasklet(小任务机制)作为下半部
        • 1.4工作队列作为下半部
        • 1.5Softirq作为下半部


内核中断机制

中断是设备中止内核的一种方法,告诉内核发生了有趣或重要的事情。这些在Linux系统上被称作IRQ。中断的主要优点是避免对设备的轮询,由设备上报自身状态的改变,而不是由内核去轮询设备状态。为获取中断通知,需要注册到IRQ,提供一个称作中断处理程序的函数,在每次中断发生的时候,将调用这个函数。

1.注册中断处理函数

可以注册一个回调函数,当感兴趣的中断(或中断线)发生时运行它。这可以通过函数request_irq()实现,该函数在中声明:

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

request_irq()可能会失败,成功则返回0。下面详细说明该函数中的一些元素:

  • flags:这是一些位掩码,定义在中。常用的掩码值如下。
  • IRQF_TIMER:通知内核这个处理程序是由系统定时器中断触发的。
  • IRQF_SHARED:用于两个或多个设备共享的中断线。共享这个中断线的所有设备都必须设置该标志。如果被忽略,将只能为该中断线注册一个处理程序。
  • IRQ_ONESHOT:主要在线程中断中使用,它要求内核在硬中断处理程序没有完成之前,不要重新启用该中断。在线程处理程序运行之前,中断会一直保持禁用状态。
  • ·name:内核用来标识/proc/interrupts和/proc/irq中的驱动程序。

  • ·dev:其主要用途是作为参数传递给中断处理程序,这对每个中断处理程序都是唯一的,因为它用来标识这个设备。对于非共享中断,它可以是NULL,但共享中断不能为NULL。使用它的常见方法是提供设备结构,因为它既独特,又可能对处理程序有用。也就是说,指向有一个指向设备数据结构的指针就足够了

  • handler:这是在中断发生时运行的回调函数。中断处理程序的结构如下:

static irqreturn_t my_irq_handler(int irq, void *dev)

它包含下面的代码元素:

irq:IRQ数值(与request_irq中的作用相同)。
dev:和reqeust_irq中的dev作用相同。

中断处理程序只有两个返回值,这取决于设备是否发起IRQ。
IRQ_NONE:设备不是中断的发起者(在共享中断线上尤其会出现这种情况)。
IRQ_HANDLED:设备引发中断。根据处理的不同,可以使用IRQ_RETVAL(val)宏,如果值为非零,则返回IRQ_HANDLED;否 则返回IRQ_NONE

释放已经注册的中断处理程序的相关函数如下

void free_irq(unsigned int irq, void *dev)

如果指定的IRQ不是共享的,那么free_irq不会删除中断处理程序,而仅仅是禁用中断线。如果IRQ是共享的,则只有删除通过dev(应该与request_irq中使用的相同)确定的中断处理程序,但是中断线依然保留,直到最后一个中断处理程序被删除后,中断线才会被禁用。free_irq会阻塞,直到指定IRQ的所有正在执行的中断完成。必须避免在中断上下文中使用request_irq和free_irq。

在计算机系统中,"中断线"通常指的是连接中断源(例如硬件设备)和中断控制器的物理线路。当一个设备需要CPU的注意时,它会通过中断线发送一个信号到中断控制器。"中断线"也可能指的是CPU的中断请求线(IRQ)。

编写中断处理程序时,不必担心重入问题,为了避免中断嵌套,内核通常会在进入中断服务程序时禁用所有的中断线。

中断处理程序和锁

**中断处理程序运行在原子上下文中,只能使用自旋锁控制并发。**每当有全局数据可供用户代码(用户任务,即系统调用)和中断代码访问时,此共享数据应受用户代码中spin_lock_irqsave()的保护。

原子上下文:在原子上下文中,内核不能访问用户空间,而且内核是不能睡眠的。也就是说在这种情况下,内核是不能调用有可能引起睡眠的任何函数。一般来讲原子上下文指的是在中断或软中断中,以及在持有自旋锁的时候。原子操作(atomic operation)意为"不可被中断的一个或一系列操作"

中断处理程序的优先级总是高于用户任务,即使该任务持有旋锁。仅仅禁用IRQ是不够的。在另一个CPU上可能会发生中断。如果更新数据的用户任务被尝试访问相同数据的中断处理程序中断,那将是一场灾难。使用spin_lock_irqsave()将禁用本地CPU上的所有中断,防止系统调用被任何类型的中断所中断:

ssize_t my_read(struct file *filp, char __user*buf, size_t count,loff_t *f_pos)
{
		unsigned long flags;
		/* 一些代码 */
		[...]
		unsigned long flags;
		spin_lock_irqsave(&my_lock, flags);
		data++;
		spin_unlock_irqrestore(&my_lock, flags)
		[...]
}
static irqreturn_t my_interrupt_handler(int irq,void *p)
{
/*
* 在运行中断处理程序时会禁用抢占
* 服务的IRQ线路被禁用,直到处理程序完成
* 不需要禁用所有其他的IRQ,只用spin_lock和spin_unlock
*/
		spin_lock(&my_lock);
		/* 处理数据 */
		[...]
		spin_unlock(&my_lock);
		return IRQ_HANDLED;
}

两个不同的中断处理程序间共享数据时(也就是同一个驱动程序管理两个或多个设备,每一个设备都有其自己的中断线),在这些处理程序中还应该使用spin_lock_irqsave()来保护共享数据,以防止其他IRQ触发和无用的自旋。

2.下半部的概念

下半部(Bottom halve)是一种把中断处理程序分成两部分的机制,这引入了另一个术语——上半部。

1.1问题——中断处理程序的设计限制

无论中断处理程序是否持有自旋锁,在运行该中断处理程序的CPU上都会禁止抢占。中断处理程序浪费的时间越多,该CPU给予其他任务的CPU时间就越少,这可能会增大其他中断的延迟,从而增加整个系统的延迟。要保持系统的响应,挑战在于尽快确认引发中断的设备

在Linux系统上(实际上在所有操作系统上,硬件设计决定),任何中断处理程序运行时,都会在所有处理器上禁用其当前中断线,有时可能需要在实际运行处理程序的CPU上禁止所有中断,但绝对不会错过中断。为了满足这一需要,引入了半部的概念。

1.2解决方案——下半部

这个方案把中断处理过程分成两个部分:

  • 第一部分称作上半部或者硬IRQ,它使用request_irq()注册函数,最终将根据需要屏蔽/隐藏中断(在当前CPU上,正在服务的中断除外,因为内核在运行该处理程序之前已经禁用它),执行快速操作(实际上是时间敏感任务,读/写硬件寄存器,以及快速处理此数据),调度第二部分和下一部分,然后确认中断线。禁用的所有中断都必须在退出下半部之前重新启用。
  • 第二部分称作下半部,会处理一些比较耗时的任务,在它执行期间,中断再次启用。这样就不会错过中断。

下半部设计使用了工作延迟机制,这在前面已经介绍过。根据选择不同,下半部可以运行在(软件)中断上下文,也可运行在进程上下文。下半部机制如下:

  • oftirq。
  • Tasklet。
  • 工作队列。
  • 线程IRQ。
  • Softirq和Tasklet在(软件)中断上下文(这意味着禁止抢占)中执行,工作队列和线程IRQ在进程(或者只是任务)上下文中执行,并且可以被抢占,但是,这不妨碍根据需求来改变它们的实时属性和抢占行为(见CONFIG_PREEMPT和CONFIG_PREEMPT_VOLUNTARY,这同样会影响整个系统)。下半部并不总是可能的,但如果有可能,则肯定是最好的选择。
1.3Tasklet(小任务机制)作为下半部

Tasklet延迟机制大多数情况下在DMA、网络和块设备驱动程序中。

struct my_data {
		int my_int_var;
		struct tasklet_struct the_tasklet;
		int dma_request;
};
static void my_tasklet_work(unsigned long data)
{
		/* 代码 */
}
struct my_data *md = init_my_data;
/* 在probe或init函数中的某个位置*/
[...]
tasklet_init(&md->the_tasklet,my_tasklet_work,(unsigned long)md);
[...]
static irqreturn_t my_irq_handler(int irq, void*dev_id)
{
		struct my_data *md = dev_id;
		/* 安排Tasklet */
		tasklet_schedule(&md.dma_tasklet);
		return IRQ_HANDLED;
}

在上面的例子中,Tasklet将执行函数my_tasklet_work()。

1.4工作队列作为下半部
static DECLARE_WAIT_QUEUE_HEAD(my_wq); 
/* 声明并初始化等待队列 */
static struct work_struct my_work;
/* probe函数的某些地方 */
/*
*工作队列的初始化。work_handler是将要返回的调用
*/
INIT_WORK(my_work, work_handler);
static irqreturn_t my_interrupt_handler(int irq,void *dev_id)
{
		uint32_t val;
		struct my_data = dev_id;
		val = readl(my_data->reg_base + REG_OFFSET);
		if (val == 0xFFCD45EE)) {
				my_data->done = true;
				wake_up_interruptible(&my_wq);
		} else {
				schedule_work(&my_work);
		}
		return IRQ_HANDLED;
};

上面的示例使用等待队列或工作队列来唤醒正在等待而可能睡眠的进程,或者根据寄存器的值来调度工作。由于没有共享数据或资源,因此不需要禁用其他的IRQ(spin_lock_irq_disable)

1.5Softirq作为下半部

在任何需要使用Softirq的地方,使用Tasklet就足够了

Softirq在软件中断上下文中运行,并且禁用抢占,保持CPU直到它们完成。Softirq应该快速执行,否则会减慢系统速度。无论什么原因,当Softirq阻止内核调度其他任务时,任何新传入的Softirq都将由在进程上下文中运行的**Ksoftirqd线程**处理。

你可能感兴趣的:(Linux系统之路,linux,单片机,运维,c语言)