• 【原创】Linux中断子系统(三)-softirq和tasklet


    背景

    • 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能尽快完成处理,那么为什么需要这么设计呢?看一张图就明白了:

    • ARM处理器在进行中断处理时,处理器进行异常模式切换,此时会将中断进行关闭,处理完成后再将中断打开;
    • 如果中断不分上下半部处理,那么意味着只有等上一个中断完成处理后才会打开中断,下一个中断才能得到响应。当某个中断处理处理时间较长时,很有可能就会造成其他中断丢失而无法响应,这个显然是难以接受的,比如典型的时钟中断,作为系统的脉搏,它的响应就需要得到保障;
    • 中断分成上下半部处理可以提高中断的响应能力,在上半部处理完成后便将中断打开(通常上半部处理越快越好),这样就可以响应其他中断了,等到中断退出的时候再进行下半部的处理;
    • 中断的Bottom-half机制,包括了softirqtaskletworkqueue、以及前文中提到过的中断线程化处理等,其中tasklet又是基于softirq来实现的,这也是本文讨论的主题;

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

    • 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);
    

    来一张图吧:

    • softirq_vec[]数组,类比硬件中断描述符表irq_desc[],通过软中断号可以找到对应的handler进行处理,比如图中的tasklet_action就是一个实际的handler函数;
    • 软中断可以在不同的CPU上并行运行,在同一个CPU上只能串行执行;
    • 每个CPU维护irq_cpustat_t状态结构,当某个软中断需要进行处理时,会将该结构体中的__softirq_pending字段或上1UL << XXX_SOFTIRQ

    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 软中断执行之一:中断处理后

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

    • Linux中断子系统(二)-通用框架处理文章中讲述了整个中断处理流程,在接收到中断信号后,处理器进行异常模式切换,并跳转到异常向量表处进行执行,关键的流程为:el0_irq->irq_handler->handle_arch_irq(gic->handle_irq)->handle_domain_irq->__handle_domain_irq
    • __handle_domain_irq函数中,irq_enterirq_exit分别用于来标识进入和离开硬件中断上下文处理,这个从preempt_count_add/preempt_count_sub来操作HARDIRQ_OFFSET可以看出来,这也对应到了上文中的Context描述图;
    • 在离开硬件中断上下文后,如果!in_interrupt() && local_softirq_pending为真,则进行软中断处理。这个条件有两个含义:1)!in_interrupt()表明不能处在中断上下文中,这个范围包括in_nmiin_irqin_softirq(Bottom-half disable)in_serving_softirq,凡是处于这几种状态下,软中断都不会被执行;2)local_softirq_pending不为0,表明有软中断处理请求;

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

    • invoke_softirq函数中,根据中断处理是否线程化进行分类处理,如果中断已经进行了强制线程化处理(中断强制线程化,需要在启动的时候传入参数threadirqs),那么直接通过wakeup_softirqd唤醒内核线程来执行,否则的话则调用__do_softirq函数来处理;
    • Linux内核会为每个CPU都创建一个内核线程ksoftirqd,通过smpboot_register_percpu_thread函数来完成,其中当内核线程运行时,在满足条件的情况下会执行run_ksoftirqd函数,如果此时有软中断处理请求,调用__do_softirq来进行处理;

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

    • local_softirq_pending函数用于读取__softirq_pending字段,可以类比于设备驱动中的状态寄存器,用于判断是否有软中断处理请求;
    • 软中断处理时会关闭Bottom-half,处理完后再打开;
    • 软中断处理时,会打开本地中断,处理完后关闭本地中断,这个地方对应到上文中提到的Top-halfBottom-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循环的次数不大于10次;
        这三个条件的判断,是基于延迟和公平的考虑,既要保证软中断尽快处理,又不能让软中断处理一直占据系统,正所谓trade-off的艺术;

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

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

    • 可以在中断处理过程中调用raise_softirq来进行软中断处理请求,处理的实际也就是上文中提到过的irq_exit退出硬件中断上下文之后再处理;
    • raise_softirq_irqoff函数中,最终会调用到or_softirq_pending,该函数会去读取本地CPU的irq_stat__softirq_pending字段,然后将对应的软中断号给置位,表明有该软中断的处理请求;
    • raise_softirq_irqoff函数中,会判断当前的请求的上下文环境,如果不在中断上下文中,就可以通过唤醒内核线程来处理,如果在中断上下文中处理,那就不执行;
    • 多说一句,在软中断整个处理流程中,会经常看到in_interrupt()的条件判断,这个可以确保软中断在CPU上的串行执行,避免嵌套;

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

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

    • 在讨论并发专题的时候,我们谈到过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字段的值保留了1,把抢占给关闭了,当do_softirq执行完毕后,再调用preempt_count_dec再减去剩下的1,进而打开抢占;
    • 为什么在使能Bottom-half时要进行软中断处理呢?在并发处理时,可能已经把Bottom-half进行关闭了,如果此时中断来了后,软中断不会被处理,在进程上下文中打开Bottom-half时,这时候就会检查是否有软中断处理请求了;

    3. tasklet

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

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

    3.1 数据结构

    • DEFINE_PER_CPU(struct tasklet_head, tasklet_vec)为每个CPU都分配了tasklet_head结构,该结构用来维护struct tasklet_struct链表,需要放到该CPU上运行的tasklet将会添加到该结构的链表中,内核中为每个CPU维护了两个链表tasklet_vectasklet_vec_hi,对应两个不同的优先级,本文以tasklet_vec为例;
    • struct tasklet_structtasklet的抽象,几个关键字段如图所示,通过next来链接成链表,通过state字段来标识不同的状态以确保能在CPU上串行执行,func函数指针在调用task_init()接口时进行初始化,并在最终触发软中断时执行;

    3.2 流程分析

    • tasklet本质上是一种软中断,所以它的调用流程与上文中讨论的软中断流程是一致的;
    • 调度tasklet运行的接口是tasklet_schedule,如果tasklet没有被调度则进行调度处理,将该tasklet添加到CPU对应的链表中,然后调用raise_softirq_irqoff来触发软中断执行;
    • 软中断执行的处理函数是tasklet_action,这个在softirq_init函数中通过open_softirq函数进行注册的;
    • tasklet_action函数,首先将该CPU上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);
    

    收工!

    欢迎关注个人公众号,不定期分享Linux内核机制文章。

  • 相关阅读:
    1.7 Matrix Zero
    1.6 Image Rotation
    Snake Sequence
    安装 Docker
    开源蓝牙协议栈 BTstack学习笔记
    无法从 repo.msys2.org : Operation too slow. Less than 1 bytes/sec transferred the last 10 seconds 获取文件
    KEIL生成预编译文件
    Duff's device
    Pyinstaller : unable to find Qt5Core.dll on PATH
    HCI 获取蓝牙厂商信息
  • 原文地址:https://www.cnblogs.com/LoyenWang/p/13124803.html
Copyright © 2020-2023  润新知