Linux 中断子系统之tasklet

Linux 中断子系统之tasklet

1 前言

中断服务程序一般都是在中断请求关闭的条件下执行的,以避免嵌套而使中断控制复杂化。但是,中断是一个随机事件,它随时会到来,如果关中断的时间太长,CPU就不能及时响应其他的中断请求,从而造成中断的丢失。因此,内核的目标就是尽可能快的处理完中断请求,尽其所能把更多的处理向后推迟。例如,假设一个数据块已经到达了网线,当中断控制器接受到这个中断请求信号时,Linux内核只是简单地标志数据到来了,然后让处理器恢复到它以前运行的状态,其余的处理稍后再进行(如把数据移入一个缓冲区,接受数据的进程就可以在缓冲区找到数据)。因此,内核把中断处理分为两部分:上半部(top half)和下半部(bottom half),上半部(就是中断服务程序)内核立即执行,而下半部(就是一些内核函数)留着稍后处理。
首先,一个快速的“上半部”来处理硬件发出的请求,它必须在一个新的中断产生之前终止。通常,除了在设备和一些内存缓冲区(如果你的设备用到了DMA,就不止这些)之间移动或传送数据,确定硬件是否处于健全的状态之外,这一部分做的工作很少。下半部运行时是允许中断请求的,而上半部运行时是关中断的,这是二者之间的主要区别。
但是,内核到底什时候执行下半部,以何种方式组织下半部?这就是我们要讨论的下半部实现机制,这种机制在内核的演变过程中不断得到改进,在以前的内核中,这个机制叫做bottom half(简称bh),在2.4以后的版本中有了新的发展和改进,改进的目标使下半部可以在多处理机上并行执行,并有助于驱动程序的开发者进行驱动程序的开发。下面主要介绍常用的小任务(Tasklet)机制及2.6内核中的工作队列机制。除此之外,还简要介绍2.4以前内核中的下半部和任务队列机制。

2 小任务机制

这里的小任务是指对要推迟执行的函数进行组织的一种机制。其数据结构为tasklet_struct,每个结构代表一个独立的小任务,其定义如下:

struct tasklet_struct {
	struct tasklet_struct *next; /*指向链表中的下一个结构*/
	unsigned long state; /* 小任务的状态 */
	atomic_t count; /* 引用计数器 */
	void (*func) (unsigned long); /* 要调用的函数 */
	unsigned long data; /* 传递给函数的参数 */
}

结构中的func域就是下半部中要推迟执行的函数 ,data是它唯一的参数。state域的取值为TASKLET_STATE_SCHED或TASKLET_STATE_RUN。TASKLET_STATE_SCHED表示小任务已被调度,正准备投入运行,TASKLET_STATE_RUN表示小任务正在运行。TASKLET_STATE_RUN只有在多处理器系统上才使用,单处理器系统什么时候都清楚一个小任务是不是正在运行(它要么就是当前正在执行的代码,要么不是)。count域是小任务的引用计数器。如果它不为0,则小任务被禁止,不允许执行;只有当它为零,小任务才被激活,并且在被设置为挂起时,小任务才能够执行。

static inline void tasklet_disable(struct tasklet_struct *t) 
{ 
    tasklet_disable_nosync(t);-------给tasklet的count加一 
    tasklet_unlock_wait(t);-----如果该tasklet处于running状态,那么需要等到该tasklet执行完毕 
    smp_mb(); 
}
static inline void tasklet_enable(struct tasklet_struct *t) 
{ 
    smp_mb__before_atomic(); 
    atomic_dec(&t->count);-------给tasklet的count减一 
}

tasklet_disable和tasklet_enable支持嵌套,但是需要成对使用。

3 小任务的管理

系统中的每个cpu都会维护一个tasklet的链表,定义如下:

static DEFINE_PER_CPU(struct tasklet_head, tasklet_vec); 
static DEFINE_PER_CPU(struct tasklet_head, tasklet_hi_vec);

linux kernel中,和tasklet相关的softirq有两项,HI_SOFTIRQ用于高优先级的tasklet,TASKLET_SOFTIRQ用于普通的tasklet。对于softirq而言,优先级就是出现在softirq pending register(__softirq_pending)中的先后顺序,位于bit 0拥有最高的优先级,也就是说,如果有多个不同类型的softirq同时触发,那么执行的先后顺序依赖在softirq pending register的位置,kernel总是从右向左依次判断是否置位,如果置位则执行。HI_SOFTIRQ占据了bit 0,其优先级甚至高过timer,需要慎用(实际上,我grep了内核代码,似乎没有发现对HI_SOFTIRQ的使用)。当然HI_SOFTIRQ和TASKLET_SOFTIRQ的机理是一样的。

4 声明和使用小任务

大多数情况下,为了控制一个寻常的硬件设备,小任务机制是实现下半部的最佳选择。小任务可以动态创建,使用方便,执行起来也比较快。我们既可以静态地创建小任务,也可以动态地创建它。选择那种方式取决于到底是想要对小任务进行直接引用还是一个间接引用。如果准备静态地创建一个小任务(也就是对它直接引用),使用下面两个宏中的一个:

DECLARE_TASKLET(name, func, data)
DECLARE_TASKLET_DISABLED(name, func, data)

这两个宏都能根据给定的名字静态地创建一个tasklet_struct结构。当该小任务被调度以后,给定的函数func会被执行,它的参数由data给出。这两个宏之间的区别在于引用计数器的初始值设置不同。第一个宏把创建的小任务的引用计数器设置为0,因此,该小任务处于激活状态。另一个把引用计数器设置为1,所以该小任务处于禁止状态。例如:
DECLARE_TASKLET(my_tasklet, my_tasklet_handler, dev); 这行代码其实等价于struct tasklet_struct my_tasklet = { NULL, 0, ATOMIC_INIT(0),tasklet_handler, dev};
这样就创建了一个名为my_tasklet的小任务,其处理程序为tasklet_handler,并且已被激活。当处理程序被调用的时候,dev就会被传递给它。

5 编写小任务的处理程序

小任务处理程序必须符合如下的函数类型:

void tasklet_handler(unsigned long data)

由于小任务不能睡眠,因此不能在小任务中使用信号量或者其它产生阻塞的函数。但是小任务运行时可以响应中断。

6 调度小任务

通过调用tasklet_schedule()函数并传递给它相应的tasklt_struct指针,该小任务就会被调度以便适当的时候执行:

static inline void tasklet_schedule(struct tasklet_struct *t) 
{ 
if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state)) 
__tasklet_schedule(t); 
}

在小任务被调度以后,只要有机会它就会尽可能早的运行。在它还没有得到运行机会之前,如果一个相同的小任务又被调度了,那么它仍然只会运行一次。可以调用tasklet_disable()函数来禁止某个指定的小任务。如果该小任务当前正在执行,这个函数会等到它执行完毕再返回。调用tasklet_enable()函数可以激活一个小任务,如果希望把以DECLARE_TASKLET_DISABLED()创建的小任务激活,也得调用这个函数,如:

tasklet_disable(&my_tasklet); /* 小任务现在被禁止,这个小任务不能运行 */
tasklet_enable(&my_tasklet); /* 小任务现在被激活 */

也可以调用tasklet_kill()函数从挂起的队列中去掉一个小任务。该函数的参数是一个指向某个小任务的tasklet_struct的长指针。在小任务重新调度它自身的时候,从挂起的队列中移去已调度的小任务会很有用。这个函数首先等待该小任务执行完毕,然后再将它移去。

程序在多个上下文中可以多次调度同一个tasklet执行(也可能来自多个cpu core),不过实际上该tasklet只会一次挂入首次调度到的那个cpu的tasklet链表,也就是说,即便是多次调用tasklet_schedule,实际上tasklet只会挂入一个指定CPU的tasklet队列中(而且只会挂入一次),也就是说只会调度一次执行。这是通过TASKLET_STATE_SCHED这个flag来完成的,我们可以用下面的图片来描述:

我们假设HW block A的驱动使用的tasklet机制并且在中断handler(top half)中将静态定义的tasklet(这个tasklet是各个cpu共享的,不是per cpu的)调度执行(也就是调用tasklet_schedule函数)。当HW block A检测到硬件的动作(例如接收FIFO中数据达到半满)就会触发IRQ line上的电平或者边缘信号,GIC检测到该信号会将该中断分发给某个CPU执行其top half handler,我们假设这次是cpu0,因此该driver的tasklet被挂入CPU0对应的tasklet链表(tasklet_vec)并将state的状态设定为TASKLET_STATE_SCHED。HW block A的驱动中的tasklet虽已调度,但是没有执行,如果这时候,硬件又一次触发中断并在cpu1上执行,虽然tasklet_schedule函数被再次调用,但是由于TASKLET_STATE_SCHED已经设定,因此不会将HW block A的驱动中的这个tasklet挂入cpu1的tasklet链表中。
下面我们再仔细研究一下底层的__tasklet_schedule函数:
在这里插入图片描述

void __tasklet_schedule(struct tasklet_struct *t) 
{ 
    unsigned long flags;

    local_irq_save(flags);-------------------(1) 
    t->next = NULL;---------------------(2*__this_cpu_read(tasklet_vec.tail) = t; 
    __this_cpu_write(tasklet_vec.tail, &(t->next)); 
    raise_softirq_irqoff(TASKLET_SOFTIRQ);----------(3local_irq_restore(flags); 
}

(1)下面的链表操作是per-cpu的,因此这里禁止本地中断就可以拦截所有的并发。
(2)这里的三行代码就是将一个tasklet挂入链表的尾部
(3)raise TASKLET_SOFTIRQ类型的softirq。

7 小任务执行的时机

上面描述了tasklet的调度,当然调度tasklet不等于执行tasklet,系统会在适合的时间点执行tasklet callback function。由于tasklet是基于softirq的,因此,我们首先总结一下softirq的执行场景:
(1)在中断返回用户空间(进程上下文)的时候,如果有pending的softirq,那么将执行该softirq的处理函数。这里限定了中断返回用户空间也就是意味着限制了下面两个场景的softirq被触发执行:
(a)中断返回hard interrupt context,也就是中断嵌套的场景
(b)中断返回software interrupt context,也就是中断抢占软中断上下文的场景

(2)上面的描述缺少了一种场景:中断返回内核态的进程上下文的场景,这里我们需要详细说明。进程上下文中调用local_bh_enable的时候,如果有pending的softirq,那么将执行该softirq的处理函数。由于内核同步的要求,进程上下文中有可能会调用local_bh_enable/disable来保护临界区。在临界区代码执行过程中,中断随时会到来,抢占该进程(内核态)的执行(注意:这里只是disable了bottom half,没有禁止中断)。在这种情况下,中断返回的时候是否会执行softirq handler呢?当然不会,我们disable了bottom half的执行,也就是意味着不能执行softirq handler,但是本质上bottom half应该比进程上下文有更高的优先级,一旦条件允许,要立刻抢占进程上下文的执行,因此,当立刻离开临界区,调用local_bh_enable的时候,会检查softirq pending,如果bottom half处于enable的状态,pending的softirq handler会被执行。

(3)系统太繁忙了,不过的产生中断,raise softirq,由于bottom half的优先级高,从而导致进程无法调度执行。这种情况下,softirq会推迟到softirqd这个内核线程中去执行。

对于TASKLET_SOFTIRQ类型的softirq,其handler是tasklet_action,我们来看看各个tasklet是如何执行的:

static void tasklet_action(struct softirq_action *a) 
{ 
struct tasklet_struct *list;

local_irq_disable();------------------(1) 
list = __this_cpu_read(tasklet_vec.head); 
__this_cpu_write(tasklet_vec.head, NULL); 
__this_cpu_write(tasklet_vec.tail, this_cpu_ptr(&tasklet_vec.head)); 
local_irq_enable();

while (list) {---------遍历tasklet链表 
struct tasklet_struct *t = list;

    list = list->next;

    if (tasklet_trylock(t)) {---------------(2if (!atomic_read(&t->count)) {--------(3if(!test_and_clear_bit(TASKLET_STATE_SCHED, &t->state)) 
                    BUG(); 
                 t->func(t->data); 
                 tasklet_unlock(t); 
                	continue;-----处理下一个tasklet 
            	} 
            tasklet_unlock(t);----清除TASKLET_STATE_RUN标记 
        }

        local_irq_disable();---------------(4) 
        t->next = NULL; 
        *__this_cpu_read(tasklet_vec.tail) = t; 
        __this_cpu_write(tasklet_vec.tail, &(t->next)); 
        __raise_softirq_irqoff(TASKLET_SOFTIRQ); -再次触发softirq,等待下一个执行时机 
        local_irq_enable(); 
    } 
}

(1)从本cpu的tasklet链表中取出全部的tasklet,保存在list这个临时变量中,同时重新初始化本cpu的tasklet链表,使该链表为空。由于bottom half是开中断执行的,因此在操作tasklet链表的时候需要使用关中断保护

(2)tasklet_trylock主要是用来设定该tasklet的state为TASKLET_STATE_RUN,同时判断该tasklet是否已经处于执行状态,这个状态很重要,它决定了后续的代码逻辑。

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

你也许会奇怪:为何这里从tasklet的链表中摘下一个本cpu要处理的tasklet list,而这个list中的tasklet已经处于running状态了,会有这种情况吗?会的,我们再次回到上面的那个软硬件结构图。同样的,HW block A的驱动使用的tasklet机制并且在中断handler(top half)中将静态定义的tasklet 调度执行。HW block A的硬件中断首先送达cpu0处理,因此该driver的tasklet被挂入CPU0对应的tasklet链表并在适当的时间点上开始执行该tasklet。这时候,cpu0的硬件中断又来了,该driver的tasklet callback function被抢占,虽然tasklet仍然处于running状态。与此同时,HW block A硬件又一次触发中断并在cpu1上执行,这时候,该driver的tasklet处于running状态,并且TASKLET_STATE_SCHED已经被清除,因此,调用tasklet_schedule函数将会使得该driver的tasklet挂入cpu1的tasklet链表中。由于cpu0在处理其他硬件中断,因此,cpu1的tasklet后发先至,进入tasklet_action函数调用,这时候,当从cpu1的tasklet摘取所有需要处理的tasklet链表中,HW block A对应的tasklet实际上已经是在cpu0上处于执行状态了。

我们在设计tasklet的时候就规定,同一种类型的tasklet只能在一个cpu上执行,因此tasklet_trylock就是起这个作用的。

(3)检查该tasklet是否处于enable状态,如果是,说明该tasklet可以真正进入执行状态了。主要的动作就是清除TASKLET_STATE_SCHED状态,执行tasklet callback function。

(4)如果该tasklet已经在别的cpu上执行了,那么我们将其挂入该cpu的tasklet链表的尾部,这样,在下一个tasklet执行时机到来的时候,kernel会再次尝试执行该tasklet,在这个时间点,也许其他cpu上的该tasklet已经执行完毕了。通过这样代码逻辑,保证了特定的tasklet只会在一个cpu上执行,不会在多个cpu上并发。

8 tasklet的简单用法

下面是tasklet的一个简单应用, 以模块的形成加载。

#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/kdev_t.h>
#include <linux/cdev.h>
#include <linux/kernel.h>
#include <linux/interrupt.h> 

static struct tasklet_struct my_tasklet; 

static void tasklet_handler (unsigned long data)
{
	printk(KERN_ALERT "tasklet_handler is running.\n");
} 

static int __init test_init(void)
{
	tasklet_init(&my_tasklet, tasklet_handler, 0);
	tasklet_schedule(&my_tasklet);
	return 0;
} 

static void __exit test_exit(void)
{
	tasklet_kill(&my_tasklet);
	printk(KERN_ALERT "test_exit running.\n");
}


module_init(test_init);
module_exit(test_exit);
MODULE_LICENSE("GPL"); 
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值