12

Linux中断子系统(三)-softirq和tasklet

 4 years ago
source link: http://mp.weixin.qq.com/s?__biz=MzI3NzA5MzUxNA%3D%3D&%3Bmid=2664607795&%3Bidx=1&%3Bsn=2ee86d36c67ea4a43332efc549a48a83
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

背景

  • Read the fucking source code! --By 鲁迅
  • A picture is worth a thousand words. --By 高尔基

说明:

  1. Kernel版本:4.14

  2. ARM64处理器,Contex-A53,双核

  3. 使用工具:Source Insight 3.5, Visio

1. 概述

中断子系统中有一个重要的设计机制,那就是 Top-half和Bottom-half ,将紧急的工作放置在 Top-half 中来处理,而将耗时的工作放置在 Bottom-half 中来处理,这样确保 Top-half 能尽快完成处理,那么为什么需要这么设计呢?看一张图就明白了:

2ueYvqb.png!web

  • ARM处理器在进行中断处理时,处理器进行异常模式切换,此时会将中断进行关闭,处理完成后再将中断打开;

  • 如果中断不分上下半部处理,那么意味着只有等上一个中断完成处理后才会打开中断,下一个中断才能得到响应。当某个中断处理处理时间较长时,很有可能就会造成其他中断丢失而无法响应,这个显然是难以接受的,比如典型的时钟中断,作为系统的脉搏,它的响应就需要得到保障;

  • 中断分成上下半部处理可以提高中断的响应能力,在上半部处理完成后便将中断打开(通常上半部处理越快越好),这样就可以响应其他中断了,等到中断退出的时候再进行下半部的处理;

  • Bottom-half
    softirq
    tasklet
    workqueue
    tasklet
    softirq
    

在中断处理过程中,离不开各种上下文的讨论,了解不同上下文的区分有助于中断处理的理解,所以,还是来一张老图吧:

Znuaqi3.png!web

  • task_struct
    thread_info.preempt_count
    context
    
  • PREEMPT_BITS :用于记录禁止抢占的次数,禁止抢占一次该值就加1,使能抢占该值就减1;
  • SOFTIRQ_BITS :用于同步处理,关掉下半部的时候加1,打开下半部的时候减1;
  • HARDIRQ_BITS :用于表示处于硬件中断上下文中;

前戏结束了,直奔主题吧。

2. softirq

2.1 初始化

softirq 不支持动态分配,Linux kernel提供了静态分配,关键的结构体描述如下,可以类比硬件中断来理解:

/* 支持的软中断类型,可以认为是软中断号, 其中从上到下优先级递减 */

enum

{

HI_SOFTIRQ=0, /* 最高优先级软中断 */

TIMER_SOFTIRQ, /* Timer定时器软中断 */

NET_TX_SOFTIRQ, /* 发送网络数据包软中断 */

NET_RX_SOFTIRQ, /* 接收网络数据包软中断 */

BLOCK_SOFTIRQ, /* 块设备软中断 */

IRQ_POLL_SOFTIRQ, /* 块设备软中断 */

TASKLET_SOFTIRQ, /* tasklet软中断 */

SCHED_SOFTIRQ, /* 进程调度及负载均衡的软中断 */

HRTIMER_SOFTIRQ, /* Unused, but kept as tools rely on thenumbering. Sigh! */

RCU_SOFTIRQ, /* Preferable RCU should always be the last softirq, RCU相关的软中断 */


NR_SOFTIRQS

};


/* 软件中断描述符,只包含一个handler函数指针 */

struct softirq_action {

void (*action)(struct softirq_action *);

};

/* 软中断描述符表,实际上就是一个全局的数组 */

static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;


/* CPU软中断状态描述,当某个软中断触发时,__softirq_pending会置位对应的bit */

typedef struct {

unsigned int __softirq_pending;

unsigned int ipi_irqs[NR_IPI];

} ____cacheline_aligned irq_cpustat_t;

/* 每个CPU都会维护一个状态信息结构 */

irq_cpustat_t irq_stat[NR_CPUS] ____cacheline_aligned;


/* 内核为每个CPU都创建了一个软中断处理内核线程 */

DEFINE_PER_CPU(struct task_struct *, ksoftirqd);

来一张图吧:

7z2I3uY.png!web

  • softirq_vec[]
    irq_desc[]
    handler
    tasklet_action
    handler
    
  • 软中断可以在不同的CPU上并行运行,在同一个CPU上只能串行执行;

  • irq_cpustat_t
    __softirq_pending
    1UL 
    			

2.2 流程分析

2.2.1 软中断注册

中断处理流程中设备驱动通过 request_irq/request_threaded_irq 接口来注册中断处理函数,而在软中断处理流程中,通过 open_softirq 接口来注册,由于它实在是太简单了,我忍不住想把代码贴上来:

void open_softirq(int nr, void (*action)(struct softirq_action *))

{

softirq_vec[nr].action = action;

}

也就是将软中断描述符表中对应描述符的 handler 函数指针指向对应的函数即可,以便软中断到来时进行回调。

那么,问题来了,什么时候进行软中断函数回调呢?

2.2.2 软中断执行之一:中断处理后

先看第一种情况,用图片来回答问题:

MVjqQ3a.png!web

  • 《Linux中断子系统(二)-通用框架处理》 文章中讲述了整个中断处理流程,在接收到中断信号后,处理器进行异常模式切换,并跳转到异常向量表处进行执行,关键的流程为: el0_irq->irq_handler->handle_arch_irq(gic->handle_irq)->handle_domain_irq->__handle_domain_irq
  • __handle_domain_irq
    irq_enter
    irq_exit
    preempt_count_add/preempt_count_sub
    HARDIRQ_OFFSET
    
  • !in_interrupt() && local_softirq_pending
    !in_interrupt()
    in_nmi
    in_irq
    in_softirq(Bottom-half disable)
    in_serving_softirq
    local_softirq_pending
    

软中断执行的入口就是 invoke_softirq ,继续分析一波:

2eeqMvj.png!web

  • invoke_softirq
    threadirqs
    wakeup_softirqd
    __do_softirq
    
  • ksoftirqd
    smpboot_register_percpu_thread
    run_ksoftirqd
    __do_softirq
    

上图中的逻辑可以看出,最终的核心处理都放置在 __do_softirq 函数中完成:

3qeMben.png!web

  • local_softirq_pending 函数用于读取 __softirq_pending 字段,可以类比于设备驱动中的状态寄存器,用于判断是否有软中断处理请求;
  • 软中断处理时会关闭 Bottom-half ,处理完后再打开;
  • 软中断处理时,会打开本地中断,处理完后关闭本地中断
    Top-half
    Bottom-half
    Bottom-half
    Bottom-half
    
  • while(softirq_bit = ffs(pending)) ,循环读取状态位,直到处理完每一个软中断请求;
  • while
    restart
    wakeup_sotfirqd
    
  1. time_before(jiffies, MAX_SOFTIRQ_TIME) ,软中断处理时间小于两毫秒;
  2. !need_resched ,当前没有进程调度的请求;
  3. max_restart = MAX_SOFTIRQ_RESTART
    restart
    trade-off
    

__do_softirq 既然可以在中断处理过程中调用,也可以在 ksoftirqd 中调用,那么 softirq 的执行可能有两种context,插张图吧:

6b6reqN.png!web

让我们来思考最后一个问题:硬件中断触发的时候是通过硬件设备的电信号,那么软中断的触发是通过什么呢?答案是通过 raise_softirq 接口:

ZbuUn2a.png!web

  • 可以在中断处理过程中调用 raise_softirq 来进行软中断处理请求,处理的实际也就是上文中提到过的 irq_exit 退出硬件中断上下文之后再处理;
  • raise_softirq_irqoff
    or_softirq_pending
    irq_stat
    __softirq_pending
    
  • raise_softirq_irqoff 函数中,会判断当前的请求的上下文环境,如果不在中断上下文中,就可以通过唤醒内核线程来处理,如果在中断上下文中处理,那就不执行;
  • 多说一句,在软中断整个处理流程中,会经常看到 in_interrupt() 的条件判断,这个可以确保软中断在CPU上的串行执行,避免嵌套;

2.2.3 软中断执行之二:Bottom-half Enable后

第二种软中断执行的时间点,在 Bottom-half 使能的时候,通常用于并发处理,进程空间上下文中进行调用:

VRFVzey.png!web

  • 在讨论并发专题的时候,我们谈到过 Bottom-half 与进程之间能产生资源争夺的情况,如果在软中断和进程之间有临界资源(软中断上下文优先级高于进程上下文),那么可以在进程上下文中调用 local_bh_disable/local_bh_enable 来对临界资源保护;
  • 图中左侧的函数,都是用于打开 Bottom-half 的接口,可以看出是 spin_lock_bh/read_lock_bh/write_lock_bh 等并发处理接口的变种形式调用;
  • __local_bh_enable_ip 函数中,首先判断调用该本接口时中断是否是关闭的,如果已经关闭了再操作BH接口就会告警;
  • preempt_count_sub
    preempt_count_add
    thread_info->preempt_count
    __local_bh_enable_ip
    cnt
    preempt_count_sub(cnt-1)
    preempt_count_dec
    preempt_count_sub(cnt-1)
    thread_info->preempt_count
    do_softirq
    preempt_count_dec
    
  • Bottom-half
    Bottom-half
    Bottom-half
    

3. tasklet

从上文中分析可以看出, tasklet 是软中断的一种类型,那么两者有啥区别呢?先说结论吧:

  • 软中断类型内核中都是静态分配,不支持动态分配,而 tasklet 支持动态和静态分配,也就是驱动程序中能比较方便的进行扩展;
  • 软中断可以在多个CPU上并行运行,因此需要考虑可重入问题,而 tasklet 会绑定在某个CPU上运行,运行完后再解绑,不要求重入问题,当然它的性能也就会下降一些;

3.1 数据结构

NbmYFnJ.png!web

  • DEFINE_PER_CPU(struct tasklet_head, tasklet_vec)
    tasklet_head
    struct tasklet_struct
    tasklet
    tasklet_vec
    tasklet_vec_hi
    tasklet_vec
    
  • struct tasklet_struct
    tasklet
    next
    state
    func
    task_init()
    

3.2 流程分析

ZRjUbim.png!web

  • tasklet 本质上是一种软中断,所以它的调用流程与上文中讨论的软中断流程是一致的;
  • tasklet
    tasklet_schedule
    tasklet
    tasklet
    raise_softirq_irqoff
    
  • tasklet_action
    softirq_init
    open_softirq
    
  • tasklet_action
    tasklet_vec
    list
    list
    t->func()
    continue
    tasklet
    tasklet_vec
    

3.3 接口

简单贴一下接口吧:

/* 静态分配tasklet */

DECLARE_TASKLET(name, func, data)


/* 动态分配tasklet */

void tasklet_init(struct tasklet_struct *t, void (*func)(unsigned long), unsigned long data);


/* 禁止tasklet被执行,本质上是增加tasklet_struct->count值,以便在调度时不满足执行条件 */

void tasklet_disable(struct tasklet_struct *t);


/* 使能tasklet,与tasklet_diable对应 */

void tasklet_enable(struct tasklet_struct *t);


/* 调度tasklet,通常在设备驱动的中断函数里调用 */

void tasklet_schedule(struct tasklet_struct *t);


/* 杀死tasklet,确保不被调度和执行, 主要是设置state状态位 */

void tasklet_kill(struct tasklet_struct *t);

收工!

如果觉得文章对您有帮助,那就点个 在看 吧,谢谢。

7vi6n22.jpg!web


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK