Linux中的中断处理机制[六]--从tsklet到中断线程化

adtxl
2023-04-26 / 0 评论 / 158 阅读 / 正在检测是否收录...

转载自https://zhuanlan.zhihu.com/p/89913872, 作者兰新宇

imagec65a75a97b998916.png

1. tasklet

tasklet虽然名字中含有一个"task",但它跟我们熟知的多线程环境中的那个"task"并没有什么直接的关系。前文介绍的9种softirq中,有两种是和tasklet相关的,一个是HI_SOFTIRQ,一个是TASKLET_SOFTIRQ。

image99ee21f6c9969f22.png

由于在实际场景中TASKLET_SOFTIRQ被使用的更多,接下来就以TASKLET_SOFTIRQ为例来讲解tasklet的具体实现。

tasklet作为bottom half的一种实现,自然也有对应的执行函数。当一个hardirq对应的bottom half被设置为tasklet模式时,hardirq执行完后会触发一个tasklet,而这个tasklet将以 tasklet_struct 的形式,挂载在由tasklet组成的链表上。在tasklet_struct中,"func"和"data"分别是tasklet的执行函数和传入参数。

struct tasklet_struct
{
    struct tasklet_struct *next;
    void (*func)(unsigned long);
    unsigned long data;
};

TASKLET_SOFTIRQ作为softirq的一种,占据了softirq的pending位图中的一个bit,如果有待处理的tasklet,那么TASKLET_SOFTIRQ在位图中对应的bit将被置为1。不同于softirq是按照pending位图从右到左来顺序执行的,当TASKLET_SOFTIRQ获得执行权时,内核将遍历tasklet_struct链表,依次执行各个tasklet注册的处理函数。

image2804662966d4c0b8.png

可见,tasklet 是基于 softirq 机制实现的,其执行也是完全融合在 softirq 机制的整体执行中的。

tasklet_schedule() --> raise_softirq_irqoff(TASKLET_SOFTIRQ)

相比起其他普通的 softirq,tasklet 支持在运行过程中动态注册,其可扩展性更强,很适合 driver 模块使用。

在 tasklet 的实现中,还有一点和其他的 softirq 不同,那就是它不支持同一种 tasklet(tasklet_struct 相同的就是同一种)在不同的CPU上执行,所以一个 tasklet 在执行前,会首先检测其他CPU上是否有正在执行的同种tasklet(这点和 hardirq 是一样的,参考这篇文章)。

以 2.6.12 版本为例,具体的做法是:检查 tasklet_struct 中的 TASKLET_STATE_RUN 这个 bit 是否有被置 1:

tasklet_action --> tasklet_trylock

static inline int tasklet_trylock(struct tasklet_struct *t)
{
    return !test_and_set_bit(TASKLET_STATE_RUN, &(t)->state);
}

这里的 trylock 虽然看起来是 bit 的原子操作,但其实起的就是 spinlock 的作用(据 Greg K-H 在 LDD3 一书中的描述,更推荐直接调用 spinlock 的 API 的做法,因为 spinlock 的实现考虑了更多因素,更 robust)。

普通的 softirq 可能 parallel 执行,因此需要在处理函数的内部考虑并行性的问题,必要时得加 spinlock 保护,tasklet 不允许 parallel 执行,减少了其处理函数实现的复杂度,但在进入之前,spinlock 的判断依然少不了。

2. workqueue

workqueue是将中断处理中可以延后执行的部分作为任务(work item)放在一个队列(queue)中,由一个或多个单独的内核线程(worker)依次执行。

与softirq/tasklet底半部机制不同的是,处理workqueue的worker线程始终运行在进程上下文中,可以睡眠。如果延后执行的部分需要睡眠,那么就只能使用workqueue,而不能使用softirq/tasklet。

imagef703f0e68a1f8ff2.png

其实,workqueue 机制不光用于中断底半部的实现,对于一般的小型任务,也可按照这种思路,不用自己起一个线程,而是扔到 workqueue 中去处理,以节约资源开销,因此 workqueue 现在已经扩展成为内核中一种通用的异步处理手段。关于workqueue的详细介绍就不在本系列文章中展开了,而是放在后续单独的文章中。

3. 中断线程化

本系列前面的文章多次提到了“中断线程化”,这主要是考虑到实时性的要求。如果实时任务不能抢占ISR,那么其执行的延迟(latency)时间将不确定,这不符合实时性的要求。

本系列前面的文章多次提到了“中断线程化”,这主要是考虑到实时性的要求。如果实时任务不能抢占ISR,那么其执行的延迟(latency)时间将不确定,这不符合实时性的要求。

image50c92d75f1386b11.png

如果内核在编译的时候配置了CONFIG_PREEMPT选项,那么将支持线程抢占,借此就可以实现中断线程化,线程化的中断处理部分被称为threaded interrupt handler。

image5e50121089b7daa2.png

上文介绍的__do_softirq()执行次数/时间超过限制后触发ksoftirqd只是中断线程化的一种情况,更通用的是使用这篇文章介绍的request_threaded_irq(),只要没有在flag参数中指定"IRQF_NO_THREAD",那么就将按中断线程化的方式处理。

当使用ps命令查看的时候,格式为"irq/%d-%s"就是这些threaded interrupt handler,其中"d"为irq号,"s"为名称:

imagebd906108ac627071.png

这些线程是在setup_irq_thread()中创建的:

__setup_irq(unsigned int irq, struct irq_desc *desc, struct irqaction *new)
{
    if (new->thread_fn && !nested) {
        ret = setup_irq_thread(new, irq, false);
        ...
}

setup_irq_thread(struct irqaction *new, unsigned int irq, bool secondary)
{
    struct task_struct *t;
    if (!secondary) {
        t = kthread_create(irq_thread, new, "irq/%d-%s", irq, new->name);
    ...
}

线程的入口函数存放在"new->thread_fn"中,而这个"thread_fn"就是request_threaded_irq()相较于传统的request_irq()新增的那个参数。

workqueue也是在线程中处理的,因而也算是一种广义上的中断线程化技术,但与使用单独的线程来进行处理不同,多个中断的workqueue底半部会共用一个worker线程。按理共用线程可以减少上下文切换的开销,提高效率,那为什么又有单独的这种中断线程化方案呢?

这是因为在传统的workqueue中,任务底半部进入队列后是没有严格的优先级之分的(现在新的workqueue实现主要也就分成了普通优先级和高优先级两类),而threaded interrupt handler可以通过设置处理线程的优先级,更好地保证实时性。

0

评论 (0)

取消