Linux中断 - tasklet

一、前言

对于中断处理而言,linux将其分成了两个部分,一个叫做中断handler(top half),属于不那么紧急需要处理的事情被推迟执行,我们称之deferable task,或者叫做bottom half,。具体如何推迟执行分成下面几种情况:

1、推迟到top half执行完毕

2、推迟到某个指定的时间片(例如40ms)之后执行

3、推迟到某个内核线程被调度的时候执行

对于第一种情况,内核中的机制包括softirq机制和tasklet机制。第二种情况是属于softirq机制的一种应用场景(timer类型的softirq),在本站的时间子系统的系列文档中会描述。第三种情况主要包括threaded irq handler以及通用的workqueue机制,当然也包括自己创建该驱动专属kernel thread(不推荐使用)。本文主要描述tasklet这种机制,第二章描述一些背景知识和和tasklet的思考,第三章结合代码描述tasklet的原理。

注:本文中的linux kernel的版本是4.0

二、为什么需要tasklet?

1、基本的思考

我们的驱动程序或者内核模块真的需要tasklet吗?每个人都有自己的看法。我们先抛开linux kernel中的机制,首先进行一番逻辑思考。

将中断处理分成top half(cpu和外设之间的交互,获取状态,ack状态,收发数据等)和bottom half(后段的数据处理)已经深入人心,对于任何的OS都一样,将不那么紧急的事情推迟到bottom half中执行是OK的,具体如何推迟执行分成两种类型:有具体时间要求的(对应linux kernel中的低精度timer和高精度timer)和没有具体时间要求的。对于没有具体时间要求的又可以分成两种:

(1)越快越好型,这种实际上是有性能要求的,除了中断top half可以抢占其执行,其他的进程上下文(无论该进程的优先级多么的高)是不会影响其执行的,一言以蔽之,在不影响中断延迟的情况下,OS会尽快处理。

(2)随遇而安型。这种属于那种没有性能需求的,其调度执行依赖系统的调度器。

本质上讲,越快越好型的bottom half不应该太多,而且tasklet的callback函数不能执行时间过长,否则会产生进程调度延迟过大的现象,甚至是非常长而且不确定的延迟,对real time的系统会产生很坏的影响。

2、对linux中的bottom half机制的思考

在linux kernel中,“越快越好型”有两种,softirq和tasklet,“随遇而安型”也有两种,workqueue和threaded irq handler。“越快越好型”能否只留下一个softirq呢?对于崇尚简单就是美的程序员当然希望如此。为了回答这个问题,我们先看看tasklet对于softirq而言有哪些好处:

(1)tasklet可以动态分配,也可以静态分配,数量不限。

(2)同一种tasklet在多个cpu上也不会并行执行,这使得程序员在撰写tasklet function的时候比较方便,减少了对并发的考虑(当然损失了性能)。

对于第一种好处,其实也就是为乱用tasklet打开了方便之门,很多撰写驱动的软件工程师不会仔细考量其driver是否有性能需求就直接使用了tasklet机制。对于第二种好处,本身考虑并发就是软件工程师的职责。因此,看起来tasklet并没有引入特别的好处,而且和softirq一样,都不能sleep,限制了handler撰写的方便性,看起来其实并没有存在的必要。在4.0 kernel的代码中,grep一下tasklet的使用,实际上是一个很长的列表,只要对这些使用进行简单的归类就可以删除对tasklet的使用。对于那些有性能需求的,可以考虑并入softirq,其他的可以考虑使用workqueue来取代。Steven Rostedt试图进行这方面的尝试(http://lwn.net/Articles/239484/),不过这个patch始终未能进入main line。

三、tasklet的基本原理

1、如何抽象一个tasklet

内核中用下面的数据结构来表示tasklet:

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

每个cpu都会维护一个链表,将本cpu需要处理的tasklet管理起来,next这个成员指向了该链表中的下一个tasklet。func和data成员描述了该tasklet的callback函数,func是调用函数,data是传递给func的参数。state成员表示该tasklet的状态,TASKLET_STATE_SCHED表示该tasklet以及被调度到某个CPU上执行,TASKLET_STATE_RUN表示该tasklet正在某个cpu上执行。count成员是和enable或者disable该tasklet的状态相关,如果count等于0那么该tasklet是处于enable的,如果大于0,表示该tasklet是disable的。在softirq文档中,我们知道local_bh_disable/enable函数就是用来disable/enable bottom half的,这里就包括softirq和tasklet。但是,有的时候内核同步的场景不需disable所有的softirq和tasklet,而仅仅是disable该tasklet,这时候,tasklet_disable和tasklet_enable就派上用场了。

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支持嵌套,但是需要成对使用。

2、系统如何管理tasklet?

系统中的每个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的机理是一样的,因此本文只讨论TASKLET_SOFTIRQ,大家可以举一反三。

3、如何定义一个tasklet?

你可以用下面的宏定义来静态定义tasklet:

#define DECLARE_TASKLET(name, func, data) \
struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(0), func, data }

#define DECLARE_TASKLET_DISABLED(name, func, data) \
struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(1), func, data }

这两个宏都可以静态定义一个struct tasklet_struct的变量,只不过初始化后的tasklet一个是处于eable状态,一个处于disable状态的。当然,也可以动态分配tasklet,然后调用tasklet_init来初始化该tasklet。

4、如何调度一个tasklet

为了调度一个tasklet执行,我们可以使用tasklet_schedule这个接口:

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

程序在多个上下文中可以多次调度同一个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);----------(3)
    local_irq_restore(flags);
}

(1)下面的链表操作是per-cpu的,因此这里禁止本地中断就可以拦截所有的并发。

(2)这里的三行代码就是将一个tasklet挂入链表的尾部

(3)raise TASKLET_SOFTIRQ类型的softirq。

5、在什么时机会执行tasklet?

上面描述了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)) {-----------------------(2)
            if (!atomic_read(&t->count)) {------------------(3)
                if (!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上并发。

原文地址:https://www.cnblogs.com/alantu2018/p/8447483.html

时间: 2024-10-31 12:00:00

Linux中断 - tasklet的相关文章

linux中断申请之request_threaded_irq

转载:linux中断申请之request_threaded_irq 在linux里,中断处理分为顶半(top half),底半(bottom half),在顶半里处理优先级比较高的事情,要求占用中断时间尽量的短,在处理完成后,就激活底半,有底半处理其余任务.底半的处理方式主要有soft_irq, tasklet, workqueue三种,他们在使用方式和适用情况上各有不同.soft_irq用在对底半执行时间要求比较紧急或者非常重要的场合,主要为一些subsystem用,一般driver基本上用不

linux 中断(原创)

------------------- 中断注册 --------------- 中断的注册实质上就是对指定的中断线确定一个中断处理程序,注册时需要指定的有,中断线,中断处理函数,触发方式. 其他的参数都可以是null.  最后一个参数是名字,这个可以是null, 不过最好写上名字. 这个在/proc/inperupts 最后一列,就是这里指定的名字.如果是共享中断,就不能是null了, 要设定特定的标志,要设置设备id. 申请中断时,要查一下设备datasheeet的中断标志位,看是不是共享中

(转)linux中断 软中断

在分析linux内核的中断,软中断时,先应该明确这样一个派生关系: irq ==> softirq ==> tasklet ==> bottom half ==> task queue------------------------|==> timer 中断是最初的原动力.分时系统依赖于时钟中断来定时重新调度可以运行的程序.外设通过中断来通知cpu处理相关的任务.中断处理程序是内核中一段特殊的,独立的,可运行实体.这个实体,某种程度上,是和进程或线程类似的. 由于中断需要快速

Linux中断完全分析

学习本文可以对linux中断有全面而深刻的认识.本文对Linux中断所涉及的需求.管理机制.中断实现.中断接口(上半部和下半部).驱动使用进行完全分析. 一.中断需求 软件是需求驱动的,软件的出现是为了解决需求和问题的.先知道需求,那理解代码就是为了验证已知的问题:不知道需求,那理解代码就是揣摩设计者的目的.两者相比,前者自然效率跟高. 中断是为了解决异步的需求.紧急的事情或者更高优先级的事情需要先做,就代表异步.例如,手机需要时刻优先响应用户按键或者触摸这个事件,否则用户体验就变差.信号是软件

Linux中断管理 (3)workqueue工作队列

目录: <Linux中断管理> <Linux中断管理 (1)Linux中断管理机制> <Linux中断管理 (2)软中断和tasklet> <Linux中断管理 (3)workqueue工作队列> 关键词: 工作队列的原理是把work(需要推迟执行的函数)交由一个内核线程来执行,它总是在进程上下文中执行. 工作队列的优点是利用进程上下文来执行中断下半部操作,因此工作队列允许重新调度和睡眠,是异步执行的进程上下文,它还能解决软中断和tasklet执行时间过长导

嵌入式Linux内核tasklet机制(附实测代码)

Linux 中断编程分为中断顶半部,中断底半部 中断顶半部: 做紧急,耗时短的事情,同时还启动中断底半部.中断底半部: 做耗时的事件,这个事件在执行过程可以被中断.中断底半部实现方法: tasklet,工作队列,软中断等机制实现.实际上是把耗时事件推后执行,不在中断程序执行. 什么是tasklet? Tasklet 一词的原意是"小片任务"的意思,这里是指一小段可执行的代码,且通常以函数的形式出现.这个 tasklet 绑定的函数在一个时刻只能在一个 CPU 上运行 ,tasklet(

linux中断子系统:中断号的映射与维护

写在前沿: 好久好久没有静下心来整理一些东西了,开始工作已有一个月,脑子里想整理的东西特别多.记录是一种很好的自我学习方式,静下来多思考多总结,三年的工作目标不能发生变化,作为职场菜鸟即将进入全世界半导体第一的Intel working,是机遇更是一种挑战,困难也是可想而知.脚踏实地.仰望星空,以结果为导向,以目标为准则,争取每天进步一点点. Linux内核版本:3.4.39 一. linux中断子系统的irq_desc初始化 linux内核最初的中断初始化过程入口为start_kernel.在

Linux中断(interrupt)子系统之一:中断系统基本原理

这个中断系列文章主要针对移动设备中的Linux进行讨论,文中的例子基本都是基于ARM这一体系架构,其他架构的原理其实也差不多,区别只是其中的硬件抽象层.内核版本基于3.3.虽然内核的版本不断地提升,不过自从上一次变更到当前的通用中断子系统后,大的框架性的东西并没有太大的改变. /*****************************************************************************************************/ 声明:本博内容

linux中断流程详解

异常体系比较复杂,但是linux已经准备了很多的函数和框架,但是因为中断是和具体的开发板相关,所以中断需要我们自己来处理一些方面,但是这也是很少的一部分,很多公用的处理函数内核已经实现,linux内核搭建了一个非常容易扩充的中断处理体系. 中 断系统结构涉及的方面很多,而且分布在很多的函数中,这里我主要理清一些结构和流程顺序已经在哪些函数中实现,我不知道其他人怎么样?但是我自己一开始怎 是找不到linux内核是怎么把GPIO设置成中断的,我找了很久都找不到,还有我们很多的设置,初始化等等东西好像