一、简介

由于中断会打断内核中进程的正常调度运行,所以要求中断服务程序尽可能的短小精悍;但是在实际系统中,当中断到来时,要完成工作往往进行大量的耗时处理。因此期望让中断处理程序运行得快,并想让它完成的工作量多,这两个目标相互制约,诞生——顶/底半部机制,本文主要介绍中断机制底半部的软中断的详细执行过程。
在这里插入图片描述

如需了解中断的整体过程请点击链接:****Linux中断机制详解

二、软中断过程分析

软中断可以使内核延期执行某个任务,他们的运作方式和具体的硬件类似,甚至可以说这里就是模拟的硬件中断,所以称之为软件中断也不为过。既然提到软中断,那么自然就设计到几个点:
1、软中断的注册;
2、软中断的触发;
3、软中断的处理;

内核版本中定义了10个软中断,并且系统不建议用户自己添加软中断,所以对于软中断基本用于已定义好的功用,而如果用户需要,可以使用其中的一个类型即TASKLET_SOFTIRQ
具体的软中断类型如下:

enum 
{
   HI_SOFTIRQ=0, //处理高优先级的tasklet
   TIMER_SOFTIRQ, //时钟中断相关的tasklet
   NET_TX_SOFTIRQ, //内核把数据报文传送给网卡
   NET_RX_SOFTIRQ, //内核从网卡接收数据报文
   BLOCK_SOFTIRQ, 
   BLOCK_IOPOLL_SOFTIRQ, 
   TASKLET_SOFTIRQ, 
   SCHED_SOFTIRQ, 
   HRTIMER_SOFTIRQ, 
   RCU_SOFTIRQ,  /* Preferable RCU should always be the last softirq */ 
   NR_SOFTIRQS 
};

每个CPU维护一个软中断位图__softirq_pending,其实是一个32位的字段,每一位对应一个软中断。处理软中断时会获取当前CPU的软中断位图,根据各个位的设置,进行处理。
#define local_softirq_pending() __get_cpu_var(irq_stat).__softirq_pending

2.1软中断的注册

软中断的核心机制是一张表,类似于IDT,包含32个softirq_vec结构,该结构很简单:就是一个函数地址,每个软中断对应其中的一个,所以现在也仅仅使用前10项。

struct softirq_action
{
    void (*action)(struct softirq_action *); //软中断发生时执行软中断的处理函数。
    void *data; //软中断的处理函数的参数指针。
};

系统通过open_softirq函数注册一个软中断,具体就是在softirq_vec数组中根据中断号设置其对应的处理例程。

void open_softirq(int nr, void (*action)(struct softirq_action*),void *data)
{
    softirq_vec[nr].data = data;
    softirq_vec[nr].action = action;
} 

nr是上面的一个枚举值,action便是对应软中断的处理函数。

2.2软中断的触发

Linux系统通过raise_softirq函数引发一个软中断,每个CPU有个软中断位图,有32位,最多可对应32个软中断,当置位图对应位为1时,表明触发了对应的软中断。在下次系统检查是否有软中断时就会被检测得到,从而进行处理。

void raise_softirq(unsigned int nr)
{
    unsigned long flags;

    local_irq_save(flags);
    raise_softirq_irqoff(nr);
    local_irq_restore(flags);
}

核心函数在该函数中,唤醒ksoftirq内核线程来处理软中断,源码在下面do_IRQ调用软中断方式中讲解。

inline void raise_softirq_irqoff(unsigned int nr)
{
    __raise_softirq_irqoff(nr);
    /*
     * schedule the softirq soon.
     */
     /*如果我们没有在中断上下文中(硬中断或者软中断),就唤醒软中断守护进程,否则之能等到从中断返回的过程中*/
    if (!in_interrupt())
        wakeup_softirqd();
}

2.3软中断的处理

处理时机:
软中断大概在三个地方会被检测检查被挂起的软中断:
1、当调用local_bh_enable()函数激活本地CPU的软中断时。条件满足就调用do_softirq() 来处理软中断。
2、当do_IRQ()完成硬中断处理时调用irq_exit()时调用do_softirq()来处理软中断。
3、当一个特殊内核线程ksoftirq/n被唤醒时,处理软中断。

中断上下文:
CPU处于处理中断上半部或者下半部,内核用in_interrupt来判断是否处于中断上下文。这是一个宏:

#define in_interrupt() (irq_count())
#define irq_count() (preempt_count() & (HARDIRQ_MASK | SOFTIRQ_MASK | NMI_MASK))

可以看到这里中断上下文包括硬件中断、软件中断、NMI中断。说到这里,出现了一个preempt_count(),LInux为每个进程的thread_info结构中维护了一个preempt_count字段,该字段是int型,因此有32位,用于支持内核抢占。当该字段为0的时候,表示当前允许内核抢占,否则不可以。

具体处理过程:
软中断的处理核心都在do_softirq函数,以在do_IRQ中调用软中断do_softirq处理过程分析:

unsigned int __irq_entry do_IRQ(struct pt_regs *regs)
{
    ......
    irq_exit();
}
 
void irq_exit(void)                                                                                                                     
{
        ......
        invoke_softirq();  //调用软irq处理
}   
#ifdef __ARCH_IRQ_EXIT_IRQS_DISABLED
#define invoke_softirq()   __do_softirq()
#else
#define invoke_softirq()   do_softirq()
#endif

软中断处理:

asmlinkage void do_softirq(void)
{
    __u32 pending;
    unsigned long flags;
    if (in_interrupt())//如果当前处于硬中断中,在硬中断处理函数退出时会调用irq_exit()函数来处理软中断,
      或当前软中断被禁用.所以in_interrupt()返回不为1 就没必要处理软中断,直接返回
        return;   
    local_irq_save(flags);/*保存中断寄存器的状态并禁用本地CPU的中断*/
    pending = local_softirq_pending();
    if (pending)
        __do_softirq();    
    local_irq_restore(flags);/*开启所有中断,恢复eflagS寄存器的内容*/
}

首先就会判断当前是否处于中断上下文,如果处于就直接返回,一个软中断既不能打断硬件中断也不能打断软件中断。如果不在中断上下文,就调用local_softirq_pending函数判断是否存在被触发的软中断,如果存在就进入if,调用__do_softirq函数, 否则开启中断,不做处理。

asmlinkage void __do_softirq(void)
{
    struct softirq_action *h;
    __u32 pending;
    unsigned long end = jiffies + MAX_SOFTIRQ_TIME;
    int cpu;
    unsigned long old_flags = current->flags;
    int max_restart = MAX_SOFTIRQ_RESTART;//重启次数最大限制
    /*
     * Mask out PF_MEMALLOC s current task context is borrowed for the
     * softirq. A softirq handled such as network RX might set PF_MEMALLOC
     * again if the socket is related to swap
     */
    current->flags &= ~PF_MEMALLOC;
    pending = local_softirq_pending();//获取当前CPU软中断位图
    account_irq_enter_time(current);
    __local_bh_disable(_RET_IP_, SOFTIRQ_OFFSET);//禁止本地cpu的软中断,现在本地cpu上挂起的软中断已经存入pending临时变量中了
    lockdep_softirq_enter();//标记进入softirq context
    cpu = smp_processor_id();//*取本地cpu id 号
restart://处理位图中的软中断
    /* Reset the pending bitmask before enabling irqs */
    set_softirq_pending(0);//操作对位图清零
    local_irq_enable();//开启本地cpu的硬中断
    h = softirq_vec;//获取软中断描述符表的起始地址
    do {
        if (pending & 1) {
            unsigned int vec_nr = h - softirq_vec;
            int prev_count = preempt_count();
            kstat_incr_softirqs_this_cpu(vec_nr);
            trace_softirq_entry(vec_nr);//标记进入softirq context
            h->action(h);
            trace_softirq_exit(vec_nr);
            if (unlikely(prev_count != preempt_count())) {
                printk(KERN_ERR "huh, entered softirq %u %s %p"
                       "with preempt_count %08x,"
                       " exited with %08x?\n", vec_nr,
                       softirq_to_name[vec_nr], h->action,
                       prev_count, preempt_count());
                preempt_count() = prev_count;
            }
            rcu_bh_qs(cpu);
        }
        h++;
        pending >>= 1;//每次pending右移1位,从代码里能看出按位右移操作,表明一次循环只处理 32 个软中断的回调函数
    } while (pending);
    local_irq_disable();//禁止本地CPU的硬中断
    pending = local_softirq_pending();//取本地CPU的__softirq_pending,查看是否还有新的被挂起的软中断并且检查被挂起软中断的次数小于10次,如果条件满足,继续处理新的被挂起的软中断
    if (pending) {
        if (time_before(jiffies, end) && !need_resched() &&
            --max_restart)
            goto restart;
        wakeup_softirqd();//如果有新的挂起的软中断并且处理循环次数已经够了10次,唤醒ksoftirq内核线程来处理软中断
    }
    lockdep_softirq_exit();//退出softirq context
    account_irq_exit_time(current);
    __local_bh_enable(SOFTIRQ_OFFSET);//使能本地CPU的软中断
    tsk_restore_flags(current, old_flags, PF_MEMALLOC);
}
//唤醒ksoftirq内核线程来处理软中断
void wakeup_softirqd(void)
{
    /* Interrupts are disabled: no need to stop preemption */
    struct task_struct *tsk = __get_cpu_var(ksoftirqd); //ksoftirqd 线程 
    if (tsk && tsk->state != TASK_RUNNING)
        wake_up_process(tsk); //唤醒ksoftirqd线程                                                                                       
}
 
static int ksoftirqd(void * __bind_cpu)
{
    // 设置当前进程状态为可中断的状态,
     set_current_state(TASK_INTERRUPTIBLE);                                       
    current->flags |= PF_KSOFTIRQD; // 设置当前进程不允许被挂起   
    while (!kthread_should_stop()) { //循环判断当前进程是否会停止
        preempt_disable();// 禁止当前进程被抢占。 
        if (!local_softirq_pending()) {  // 首先判断系统当前没有需要处理的 pending 状态的软中断
           // 没有的话在主动放弃 CPU 前先要允许抢占,因为一直是在不允许抢占状态下执行的代码。   
            preempt_enable_no_resched(); 
           // 主动放弃 CPU 将当前进程放入睡眠队列,并转换新的进程执行(调度器相关不记录在此),但此进程再次被唤醒时,直接执行下一语句。
            schedule();
            preempt_disable(); // 当进程再度被调度时,在以下处理期间内禁止当前进程被抢占。   
        } 
        __set_current_state(TASK_RUNNING); // 设置当前进程为运行状态。       
        /*
         * 循环判断是否有 pending 的软中断,如果有则调用 do_softirq()来做具体处理。
         * 注意:这里又是个 do_softirq() 的入口点,
         * 那么在 __do_softirq() 当中循环处理 10 次软中断的回调函数后,
         * 如果更有 pending 的话,会又调用到这里
         * 那么在这里则又会有可能去调用 __do_softirq() 来处理软中断回调函数。
         * 在__do_softirq()中,处理 10 次还处理不完的话说明系统正处于繁忙状态。
         * 综上,在系统非常繁忙时,这个进程将会和 do_softirq() 相互交替执行,
         * 这时此进程占用 CPU 应该会非常高,
         */
        while (local_softirq_pending()) {
            /* Preempt disable stops cpu going offline.
               If already offline, we'll be on wrong CPU:
               don't process */
            if (cpu_is_offline((long)__bind_cpu))// 如果当前被关联的 CPU 无法继续处理则跳转 
                goto wait_to_die;  // 到 wait_to_die 标记出,等待结束并退出。 
 
             // 执行 do_softirq() 来处理具体的软中断回调函数。
             //如果此时有一个正在处理的软中断的话,则会马上返回 注意in_interrupt()
            do_softirq();
            preempt_enable_no_resched();// 允许当前进程被抢占。
 
            // 这个函数有可能间接的调用 schedule() 来转换当前进程,而且上面已允许当前进程可被抢占。
            //也就是说在处理完一轮软中断回调函数时,有可能会转换到其他进程。
            cond_resched();
            preempt_disable(); // 禁止当前进程被抢占。
            rcu_sched_qs((long)__bind_cpu);
        }// 处理完所有软中断了吗?没有的话继续循环以上步骤   
 
        preempt_enable();// 允许当前进程被抢占
        set_current_state(TASK_INTERRUPTIBLE); // 设置当前进程状态为可中断的状态,
    }
    __set_current_state(TASK_RUNNING);// 设置当前进程为运行状态
    return 0;
 
wait_to_die:// 一直等待到当前进程被停止
    preempt_enable();
    /* Wait for kthread_stop */
    set_current_state(TASK_INTERRUPTIBLE);
    // 判断当前进程是否会被停止,如果不是的话则设置进程状态为可中断状态并放弃当前 CPU 主动转换。
    // 也就是说这里将一直等待当前进程将被停止时候才结束。   
    while (!kthread_should_stop()) {
        schedule();
        set_current_state(TASK_INTERRUPTIBLE);
    }
   // 如果将会停止则设置当前进程为运行状态后直接返回。调度器会根据优先级来使当前进程运行。
    __set_current_state(TASK_RUNNING);
    return 0;
}

软中断处理过程总结:
首先调用local_softirq_pending函数获取当前CPU软中断位图,然后调用__local_bh_disable函数禁止本地软中断,接着调用lockdep_softirq_enter函数标记进入softirq context。下面的restart段就开始处理位图中的软中断了。
进入该节的首要操作对位图清零,因为随时可能有同种类型的软中断被触发,接着就调用local_irq_enable函数开启中断。下面h = softirq_vec;是获取软中断描述符表的起始地址,进入do循环,从pending的第一位开始处理,每次pending右移1位,同时h++,所以h定位具体的软中断类型,pending判断是否被触发。如果被触发,那么进入if内部,内部就是调用了h->action(h)函数处理软中断;
在循环结束后,就再次关中断,然后重新读取pending,如果又有新的软中断被触发&&本次处理软中断未超时&&当前进进程的调度位TIF_NEED_RESCHED没有被设置&&重启次数没到最大限制,就再次执行restart节进行处理。否则只能唤醒守护进程下次再处理软中断。
之后就标记退出softirq context,开启软中断。
每个CPU都会有一个软中断守护进程ksoftirqd,同时也有一个软中断位图,我们触发的时候会指定CPU的id,各个CPU处理的软中断就不会影响,即使两个CPU处理同一类型的软中断。这样也避免了很多需要同步的操作,当然两个CPU都在处理同一类型的软中断,那么还是需要一定的同步来保障临界区的安全。如果在do_softirq的末尾有未处理的软中断,就不得不唤醒守护进程进行处理;同样在raise_softirq_irqoff中在触发指定软中断后,判断是否在中断上下文,如果不在中断上下文就唤醒守护进程,否则下次检查调度的时候处理这些软中断。
基本的处理过程就如上所述,但是还是存在不少问题,前面代码片段中出现了很多开关中断的操作,为何需要有这些操作以及这些操作的原理如何?下面我们分析一下。
开关中断涉及到的函数主要有下面几个:

local_irq_save和local_irq_restore
local_bh_disable和local_bh_enable
local_irq_enable和local_irq_disable

其中1和3是针对hard irq,而2是针对soft irq。而且以上函数都是成对出现的。
local_irq_save和local_irq_restore是保存和恢复EFLAGS寄存器的状态,首先执行local_irq_save会保存EFLAGS寄存器的状态到一个变量,然后禁止本地中断(可屏蔽的外部中断),local_irq_restore会恢复EFLAGS寄存器到之前保存的状态。
local_irq_disable会直接禁止本地中断(可屏蔽的外部中断),而local_irq_enable会打开本地中断。这些都是针对可屏蔽外部中断,对于NMI和异常没有作用。
local_bh_disable会设置当前进程的抢占计数器,即增加对应的位,这样,当前进程就标识为不可抢占,也就关闭了软件中断。为什么说这里关闭了软件中断呢?因为前面我们设置了抢占计数器,而在每次检查准备调度时候,都会判断当前是否处于中断上下文,如果处于
就不发生调度,从而不抢占当前进程。

当内核明确不允许发生抢占或内核正在中断上下文中运行时,必须禁止内核的抢占功能。为了确定当前进程是否能够被抢占,内核快速检查preempt_counte字段是否等于零。
另一个跟软中断相关的字段是每个CPU都有一个32位掩码的字段

typedef struct {
unsigned int __softirq_pending;
} ____cacheline_aligned irq_cpustat_t;

他描述挂起的软中断。每一位对应相应的软中断。比如0位代表HI_SOFTIRQ.
宏local_softirq_pending()来获取该字段的值。
使用函数raise_softirq()来激活软中断。即把响应的软中断号对应的__softirq_pending中的位置1.表示该软中断被挂起。如果当前CPU不在中断上下文中,唤醒内核线程ksoftirqd来检查被挂起的软中断,然后执行相应软中断处理函数。

Logo

更多推荐