14.中断的下半部和推后执行的工作

betball贝博app Linux 708 次浏览 没有评论
  • 工作队列的具体工作没有详细的看,见书121-127页
  • 下半部机制的选择
  • 上下文切换之间加锁
  • 禁止下半部
  • 中断处理程序的局限

  • 中断处理程序异步进行,有可能打断其他重要代码,所以中断处理程序执行时间要越短越好
  • 如果当前有一个中断处理程序正在执行,最好的情况下,同级其他中断会被屏蔽;最坏的情况下,处理器上其他所有中断都会被屏蔽。所以中断处理程序执行时间要越短越好
  • 中断处理程序往往需要对硬件操作,所以通常有很高的时限要求。
  • 不在进程上下文中运行,所以不能阻塞,这限制了他们能做的事情。
  • 所以中断适合完成快速,异步,简单并需要作出迅速响应的机制。下面研究中断处理程序中的另一部分-下半部

    下半部

    最理想的情况下中断处理程序将所有的工作交给下半部执行。

    下半部的实现环境

    完成下半部程序有多种不同的方法

  • 起源:bottom half 在当时名字很纯粹,就是将功能推后的方法,也成为BH 2.5版本已经被弃用
  • 任务队列: 尝试代替BH,但是无法完全代替,对于一些性能要求较高的子系统如网络部分无法胜任。
  • 软中断和tasklet: 2.3版本引入,如果不考虑和过去的驱动程序兼容,那么完全可以替代BH
  • 软中断是一组静态定义的下半部接口,有32个,可以在所有处理器上同步执行
    类型相同的软中断可以同时直行,所以需要小心
    软中断必须在编译期间就进行动态注册

  • tasklet是一种基于软中断实现的灵活性强、动态创建的下半部实现机制,两个不同类型的tasklet可以在不同的处理器上同时执行,但类型相同的不能同时执行。
    tasklet是一种在性能和易用性上寻求平衡的产物,对于大部分下半部处理来说,用tasklet就足够了。只有像网络这种性能要求非常高的情况才需要使用软中断。
    可以通过代码进行动态注册

    下半部机制 状态
    BH
    2.5中去除
    任务队列(Task queues)
    2.5中去除
    软中断(Softirq)
    2.5中引入,应用较少
    tasklet
    2.3中引入,常用方式
    工作队列(Work queues)
    2.5中引入
  • 软中断kernel/softirq.c

    软中断的结构体定义<linux/interrupt.h>

    struct softirq_action
    {
        void    (*action)(struct softirq_action *);
        void    *data;
    };
    

    软中断的声明 <kernel/softirq.c>

    定义了包含32个上述结构体的数组

    static struct softirq_action softirq_vec[32] __cacheline_aligned_in_smp;

    软中断处理程序

    程序原型

    该函数为修改源码时自行定义,在原始的代码中貌似找不到
    void softirq_handler(struct softirq_action *)

    执行软中断

    下列情况下会触发软中断

  • 从一个硬件中断代码处返回
  • 在ksoftirqd内核线程中
  • 在显式检查和执行待处理的软中断代码,如网络子系统中
  • 无论通过什么方法,都在do_softirq()中执行
    其中的核心过程即为遍历每一个软中断调用处理程序
    20191016104858.png

    软中断在执行的过程中允许响应中断但他自己不能休眠,在一个处理程序运行的时候,当前处理器上的软中断被禁止,但其他处理器仍可以执行别的软中断。也就是同一个软中断可以被不同的处理器同时执行,那么任何共享数据(甚至是仅在软中断内部使用的全局变量)都需要严格的加锁保护。

    因为上述原因,大部分的软中断处理程序都通过单处理器数据或其他技巧来避免显式加锁,从而实现更好的性能。

    使用软中断

  • 分配索引

  • 在<linux/interrupt.h>中定义的枚举类型中添加一项来声明软中断,索引号越小的优先级越高
  • 注册处理程序

  • 通过open_sofrirq()函数注册软中断处理程序,包含 软中断索引号,处理函数 两个参数
    open_softirq(NET_TX_SOFTIRQ,net_tx_action);
    open_softirq(NET_RX_SOFTIRQ,net_rx_action);
  • 触发软中断
    raise_softirq(NET_RX_SOFTIRQ);
    可以将软中断设置为挂起装填,下次调用do_softirq()将投入运行。

    void fastcall raise_softirq(unsigned int nr)
      {
          unsigned long flags;
    
          local_irq_save(flags);
          raise_softirq_irqoff(nr);
          local_irq_restore(flags);
      }
    

    local_irq_save() 功能为保存本地中断传递的当前状态,然后禁止本地中断

    所以如果本地中断本身就已经是禁止状态,那么可以调用raise_softirq_irqoff(nr);实现更好的效果。

  • tasklet

    tasklet和软中断很相近,但是接口更简单,锁保护要求也较低

    通常应该选用tasklet,软中断只应用于执行频率和连续性都很高的场合才使用,而tasklet有更广泛的用途

    tasklet的结构体

        struct tasklet_struct
        {
            struct tasklet_struct *next;//链表中下一个tasklet
            unsigned long state;//tasklet状态
            atomic_t count;//引用计数器
            void (*func)(unsigned long);//处理函数
            unsigned long data;//给处理函数的参数
        };
    
  • next:
  • state:
  • 0
  • TASKLET_STATE_SCHED
  • TASKLET_STATE_RUN:在多处理器系统上才会作为一种优化来使用,单处理器系统本身就知道tasklet是否在运行
  • count:引用计数器,如果不为0则该tasklet被禁止
  • func:类似于软中断中的action,data为唯一的参数
  • data
  • tasklet调度

    已调度的tasklet(等同于被触发的软中断)存放在两个单处理器数据结构,都是由tasklet_struct结构体构成的链表。链表中的每一个tasklet_struct代表一个不同的tasklet

  • tasklet_vec(普通tasklet)
  • tasklet_hi_vec(高优先级的tasklet)
  • tasklet由tasklet_schedule()和tasklet_hi_schedule()进行调度,其接受一个执行tasklet_struct结构的指针作为参数,两个函数非常类似

    tasklet_schedule的执行过程

    20191016195518.png

    20191016195540.png

    上述过程中软中断被唤醒,下次调用do_softirq()时就会通过调用tasklet_action()执行该tasklet

    tasklet的执行步骤

    20191017155029.png

    使用tasklet

  • 声明tasklet
  • 可以静态创建也可以动态创建,取决于对一个tasklet直接引用还是间接引用,静态创建具体操作使用下列函数

        DECLARE_TASKLET(name,func,data)
        DECLARE_TASKLET_DISABLE(name,func,data)
    

    前一个函数把创建的tasklet的计数器设置为0表示处于激活态,后一个设置为1处于禁止态

    动态创建使用tasklet_init(t,tasklet_handler,dev);

    编写tasklet处理程序

    void tasklet_handler(unsigned long data)

    tasklet使用软中断实现所以不能休眠,所以不能使用信号量或阻塞式函数,

    调度tasklet

    tasklet_schedule(&mytasklet);

    一个tasklet总在调度他的处理器上执行,从而可以更好的利用处理器缓存

    ksoftirqd

    每个处理器都有一组辅助处理软中断和tasklet的内核线程,当内核中出现大量的软中断时,这些内核进程就会辅助处理他们。tasklet也是通过软中断实现,所以下面主要讨论软中断

    对于软中断,内核会选择在几个特殊时期进行处理,在中断处理程序返回时处理最常见。而且比如网络子系统,会在软中断执行的时候,重新触发自己从而可以再次执行。如果软中断本身出现的频率就高而且又有执行自己的能力,那么就会导致用户空间进程无法获得足够的处理器时间而处于饥饿状态。

    软中断的直观处理策略

  • 只要还有被触发并等待处理的软中断,本次执行就要负责处理,重新触发的软中断也在本次执行返回前被处理,可以保证软中断及时处理但用户空间的任务还行效果就会变差,只有系统永远处于低负载的情况下,才会有理想效果。
  • 不处理重新触发的软中断,从中断程序返回时内核和平常一样检查所有挂起的软中断并进行处理,但是任何自行重新触发的软中断都不会马上处理,它们被放到下一个软中断执行时去处理。但是对于比较空闲的系统,立即处理中断才能有更好的效果。所以该方案时好时坏,可以保证用户空间不饥饿,但是软中断会饥饿,没有利用好限制的系统资源
  • 设计软中断的折中策略:最终在内核中设计的方案为不会立即处理重新触发的软中断,当大量软中断出现的时候,内核会唤醒一组内核线程来处理这些负载,这些线程在最低优先级运行(nice=19),可以避免与其他重要任务抢夺资源,但肯定会执行。所以可以实现保证
  • 在软中断负担很重的时候,用户程序不会因为得不到处理时间而处于饥饿状态。
  • 过量的软中断终究可以得到处理
  • 每个处理器都会一个这样的线程,叫做ksoftirqd/n,区别在于n,表示处理器的编号,如双核CPU有两个线程ksoftirqd/0和ksofirqd/1,从而只要有空闲的处理器就会处理软中断。

    一旦该线程被初始化,就会执行死循环:(在具体的源码文件中没有找到)
    20191103151843.png
    softirq_pending负责发现是否有待处理的终端,如果有的话通过do_softirq()进行处理,重复执行该操作,每次迭代调用schedule()以便让更重要的进行得到处理的机会,当所有的进程都操作完成以后,该内核线程将自己设置为TASK_INTERRUPTIBLE状态,唤醒调度程序选择其他可执行进程。

    老的BH机制

    在2.6版的内核中已经找不到该机制了
    所有的BH都是静态定义的,最多可以有32个,实现模块时不能直接使用BH接口。每个BH处理程序严格的按顺序执行,不能同时执行,不利于多处理器的可扩展性和大型SMP的性能。
    BH的缺点

    工作队列

    工作队列是一种将工作推后执行的形式,交由一个内核线程去执行。这样通过工作队列执行的代码能占尽上下文的所有优势,允许重新调度甚至睡眠。

    如果推后执行的任务需要睡眠,通常采用工作队列;反之则选择软中断或者tasklet。工作队列通常可以用内核线程替换,但是由于内核开发者反对创建新的内核线程,所以也推荐使用工作队列。

    如果需要一个可以重新调度的实体来执行下半部,应该使用工作队列,这是唯一能在进程上下文中运行的下半部实现机制。也只有它可以睡眠。这意味着在需要获得大量内存,需要获取信号量,需要执行阻塞式IO操作时,都会非常有用。

    工作队列的实现

    工作队列子系统是一个用于创建内核线程的接口,通过它创建的进程负责执行由内核其他部分排到队列中的任务。它创建的这些内核线程成为工作者线程。工作队列可以让驱动程序创建一个专门的工作者线程来处理推后的工作。工作队列子系统提供了一个缺省的工作者线程来处理这些工作,所以工作队列的最基本表现形式为把需要推后执行的任务交给特定的通用线程的一种接口。

    缺省的工作者线程为events/n,n为处理器编号,每个处理器对应一个线程。缺省的工作者线程会从多个地方得到被推后的工作。许多内核驱动程序都把他们的下半部交给缺省的工作者线程来做。除非一个驱动程序或者子系统必须建立一个属于自己的内核线程,否则最好使用缺省线程。

    工作队列的具体工作没有详细的看,见书121-127页

    下半部机制的选择

    三种可能的选择

    软中断 tasklet 工作队列
    序列化保障最少
    接口简单
    需要把任务推后到任务上下文中完成
    必须采用一些方法保证共享数据安全
    两个同类型的tasklet不能同时运行
    开销最大,需要牵扯到内核线程甚至是上下文切换
    对时间严格的应用执行速度快
    如需准备利用每一处理器上的变量以确保软中断能够安全的在多个处理器上并发运行,那么还应该选软中断
    易于使用

    如果需要一个可调度(需要休眠)的实体来执行推后完成的工作,工作队列是唯一的选择。
    专注于性能的提高,则应该选择软中断。

    上下文切换之间加锁

    使用tasklet的一个好处是它自己负责执行序列化的保障,即使在不在同一个处理器上,两个相同类型的tasklet都不允许同时执行,所以无需考虑同步问题,tasklet之间的同步需要正确的使用锁机制。

    进程上下文和下半部需要共享数据在访问数据之前需要禁止下半部的处理并得到锁的使用权,可以本地和SMP的保护并防止死锁的出现。

    工作队列中共享的数据也需要使用锁机制。

    禁止下半部

    一般单纯的禁止下半部的处理是不够的,为了保证共享数据的安全一般是先得到一个锁然后再禁止下半部的处理

    如果需要禁止所有的下半部处理(所有软中断和所有tasklet)可以调用local_bh_disable()函数。
    允许下半部处理可以调用local_bh_enable()函数。

    函数通过preempt_count为每个进程维护一个计数器,当计数器变为0时,下半部才能够被处理。

    发表评论

    邮箱地址不会被公开。

    Go