Linux内核SoftIrq源代码分析

嵌入式技术

1330人已加入

描述

我们在分析linux内核中断剖析时,简单的聊了一下SOFTIRQ, 而没有进行深入分析. Linux内核讲对一个外部设备中断的处理分成两大部分HARDIRQ以及SOFTIRQ, HARDIRQ部分在执行时处理器的中断是关闭的,所以驱动程序的中断处理例程只应该完成一些关键的中断操作,而将耗时的操作放到SOFTIRQ部分执行, 本篇文章我们将对这部分进行深入讨论.

SoftIrq的应用非常广泛, 例如我们常见的网卡在做网络包的收发, 封装好用来做延迟操作的tasklet的实现等.

SoftIrq源代码分析

首先看一下linux内核当中的irq类型, 而在softirq中维护着struct softirq_action softirq_vec[NR_SOFTIRQS]这样一个类型的数组.

//include/linux/interrupt.h 
enum
{
  HI_SOFTIRQ=0,
  TIMER_SOFTIRQ,
  NET_TX_SOFTIRQ,//网络端口TX
  NET_RX_SOFTIRQ,//网络端口RX
  BLOCK_SOFTIRQ,
  IRQ_POLL_SOFTIRQ,
  TASKLET_SOFTIRQ,//tasklet实现时使用的irq
  SCHED_SOFTIRQ,
  HRTIMER_SOFTIRQ, /* Unused, but kept as tools rely on the numbering. Sigh! */
  RCU_SOFTIRQ,    /* Preferable RCU should always be the last softirq */
  NR_SOFTIRQS
};

在kernel_start 的时候,在做完中断时间等初始化后,会进行softirq的初始化动作:

//init/main.c 
asmlinkage __visible void __init start_kernel(void){
  ...
  init_IRQ();
  ...
  init_timers();
  hrtimers_init();
  softirq_init(); //softirq初始化动作
  ...
}

softirq_init的动作很简单,做了两件事情, 第一件事情:为每个核创建分别创建了tasklet_vec(对应TASKLET_SOFTIRQ类型软中断)和tasklet_hi_vec(对应HI_SOFTIRQ类型软中断)链表. 第二件事情就是给数组对应的softirq_vec[NR_SOFTIRQS]中TASKLET_SOFTIRQ和HI_SOFTIRQ两种类型的softirq_action初始化自己的softirq_action的回调处理函数

//kernel/softirq.c 
static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;
void open_softirq(int nr, void (*action)(struct softirq_action *))
{
  softirq_vec[nr].action = action;
}


void __init softirq_init(void)
{
  int cpu;
  //第一件事情,给每个核创建一个对应链表.
  for_each_possible_cpu(cpu) {
    per_cpu(tasklet_vec, cpu).tail =
      &per_cpu(tasklet_vec, cpu).head;
    per_cpu(tasklet_hi_vec, cpu).tail =
      &per_cpu(tasklet_hi_vec, cpu).head;
  }
  //第二件事情,注册两个不同类型softirq的回调处理函数
  open_softirq(TASKLET_SOFTIRQ, tasklet_action);
  open_softirq(HI_SOFTIRQ, tasklet_hi_action);
}

接下来我们重点看一下softirq的处理核心函数__do_softirq, 它被调用的时间点是处理完中断函数后会调用irq_exit(关于这点不清楚的可以回顾一下之前的文章: Linux内核中断剖析--外部中断(上)时进行处理,这里就不再做过多介绍.我们直接分析核心函数__do_softirq

//kernel/softirq.c
void irq_exit(void)
{
#ifndef __ARCH_IRQ_EXIT_IRQS_DISABLED
  local_irq_disable();
#else
  lockdep_assert_irqs_disabled();
#endif
  account_irq_exit_time(current);
  preempt_count_sub(HARDIRQ_OFFSET);//表示HARDIRQ 
  if (!in_interrupt() && local_softirq_pending())//表示当前没有在软硬和不可中断中才可以进入,防止中断嵌套.
    invoke_softirq();//这里面会call到关键函数


  tick_irq_exit();
  rcu_irq_exit();
  trace_hardirq_exit(); /* must be last! */
}


static inline void invoke_softirq(void)
{
  if (ksoftirqd_running(local_softirq_pending()))
    return;


  if (!force_irqthreads) {
#ifdef CONFIG_HAVE_IRQ_EXIT_ON_IRQ_STACK
    /*
     * We can safely execute softirq on the current stack if
     * it is the irq stack, because it should be near empty
     * at this stage.
     */
    __do_softirq();
#else
    /*
     * Otherwise, irq_exit() is called on the task stack that can
     * be potentially deep already. So call softirq in its own stack
     * to prevent from any overrun.
     */
    do_softirq_own_stack();//内部实现也是__do_softirq.
#endif
  } else {
    wakeup_softirqd();
  }
}
//重点中的重点!!!
asmlinkage __visible void __softirq_entry __do_softirq(void)
{
  unsigned long end = jiffies + MAX_SOFTIRQ_TIME;
  unsigned long old_flags = current- >flags;
  int max_restart = MAX_SOFTIRQ_RESTART;
  struct softirq_action *h;
  bool in_hardirq;
  __u32 pending;
  int softirq_bit;


  /*
   * Mask out PF_MEMALLOC as the 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 swapping.
   */
  current- >flags &= ~PF_MEMALLOC;


  pending = local_softirq_pending();//获取被置起来的中断类型,以类型为对应的bit位
  account_irq_enter_time(current);


  __local_bh_disable_ip(_RET_IP_, SOFTIRQ_OFFSET);//表示进入softirq上下文
  in_hardirq = lockdep_softirq_start();


restart:
  /* Reset the pending bitmask before enabling irqs */
  set_softirq_pending(0);//清空被置起需要处理类型的软中断类型.


  local_irq_enable();


  h = softirq_vec;
  //ffs函数为找到对应pending的第一个bit不为0 的bit位. 
  //实际就是遍历一遍softirq_vec中被置起来的soft.
  while ((softirq_bit = ffs(pending))) {
    unsigned int vec_nr;
    int prev_count;


    h += softirq_bit - 1;


    vec_nr = h - softirq_vec;
    prev_count = preempt_count();


    kstat_incr_softirqs_this_cpu(vec_nr);


    trace_softirq_entry(vec_nr);
    h- >action(h);//调用到我们的注册的action
    trace_softirq_exit(vec_nr);
    if (unlikely(prev_count != preempt_count())) {
      pr_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_set(prev_count);
    }
    h++;
    pending > >= softirq_bit;
  }


  if (__this_cpu_read(ksoftirqd) == current)
    rcu_softirq_qs();
  local_irq_disable();


  pending = local_softirq_pending();
  if (pending) {
    if (time_before(jiffies, end) && !need_resched() &&
        --max_restart)
      goto restart;


    wakeup_softirqd();
  }


  lockdep_softirq_end(in_hardirq);
  account_irq_exit_time(current);
  __local_bh_enable(SOFTIRQ_OFFSET);
  WARN_ON_ONCE(in_interrupt());
  current_restore_flags(old_flags, PF_MEMALLOC);
}

总结一些__do_softirq 就是 在某个时间点对应类型的softirq会被置起(稍后我们以tasklet为例来进行说明,tasklet常常用在中断中的上半部进行置起)就是对应pending的bit位(每个bit位对应一个softirq的类型)上面代码第62行, 然后根据pending的bit遍历(77行)调用对应的action(89行),以开头softirq_init时注册的TASKLET_SOFTIRQ和HI_SOFTIRQ为例, 这个时候如果被置起调用的就是tasklet_action 与tasklet_hi_action 两个函数.

我们这里只剖析TASKLET_SOFIRQ类型对应的action: tasklet_action(HI_SOFTIRQ的类似), tasklet_action做的事情很简单,就是将当前核的tasklet_struct 的list列表遍历一遍, 调用所对应的注册函数(下面第29行). 我们在tasklet的使用分析时再讨论(也可以参考Linux内核中断剖析--外部中断(下)当中的tasklet部分.

//kernel/softirq.c
static __latent_entropy void tasklet_action(struct softirq_action *a)
{
  tasklet_action_common(a, this_cpu_ptr(&tasklet_vec), TASKLET_SOFTIRQ);
}
//重点函数
static void tasklet_action_common(struct softirq_action *a,
          struct tasklet_head *tl_head,
          unsigned int softirq_nr)
{
  struct tasklet_struct *list;


  local_irq_disable();
  list = tl_head- >head;
  tl_head- >head = NULL;
  tl_head- >tail = &tl_head- >head;
  local_irq_enable();


  while (list) {
    struct tasklet_struct *t = list;


    list = list- >next;


    if (tasklet_trylock(t)) {
      if (!atomic_read(&t- >count)) {
        if (!test_and_clear_bit(TASKLET_STATE_SCHED,
              &t- >state))
          BUG();
        t- >func(t- >data);//调用注册的回调函数
        tasklet_unlock(t);
        continue;
      }
      tasklet_unlock(t);
    }


    local_irq_disable();
    t- >next = NULL;
    *tl_head- >tail = t;
    tl_head- >tail = &t- >next;
    __raise_softirq_irqoff(softirq_nr);
    local_irq_enable();
  }
}

SoftIrq的使用示例tasklet

还记得我们在Linux内核中断剖析--外部中断(下)当中tasklet的使用模板吗? 我们这里再拿过来如下代码段.

//tasklet模板
void mangodan_do_tasklet(unsigned long);
DECLARE_TASKLET(mangodan_tasklet,mangodan_do_tasklet,0);
/**中断处理下半部*/
void mangodan_do_tasklet(unsigned long){  
  ...
}
/*中断处理上半部分*/
irqreturn_t mangodan_interrupt(int irq, void *dev_id){
   ...
   tasklet_schedule(&mangodan_tasklet);
   ...
}

我们继续从源代码分析一下, 首先是声明DECLARE_TASKLET, 就是初始化了一个tasklet_struct 的结构体. 注册了对应的回调函数mangodan_do_tasklet , 这个函数就是上述tasklet_action_common遍历tasklet_struct的链表中的t->func(t->data)(上述第29行);

//include/linux/interrupt.h
struct tasklet_struct
{
  struct tasklet_struct *next;
  unsigned long state;
  atomic_t count;
  void (*func)(unsigned long);
  unsigned long data;
};


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

那么声明的tasklet它是何时告知类型为TASKLET_SOFTIRQ的softirq需要被置起的呢?以上述示例为例就在于响应中断上半部时的tasklet_schedule, 这个函数干了两件事情: 将自己定义的tasklet_struct 加入到对应链表和置起TASKLET_SOFTIRQ类型, 在退出exit_irq时, __do_softirq中就会调用对应TASKLET_SOFTIRQ类型的action, 处理对应action里面就会遍历对应的tasklet_struct的list,调用对应的func注册函数.

//include/linux/interrupt.h
extern void __tasklet_schedule(struct tasklet_struct *t);


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


//kernel/softirq.c
void __tasklet_schedule(struct tasklet_struct *t)
{
  __tasklet_schedule_common(t, &tasklet_vec,
          TASKLET_SOFTIRQ); //注意传参类型
}
EXPORT_SYMBOL(__tasklet_schedule);


static void __tasklet_schedule_common(struct tasklet_struct *t,
              struct tasklet_head __percpu *headp,
              unsigned int softirq_nr)
{
  struct tasklet_head *head;
  unsigned long flags;


  local_irq_save(flags);
  head = this_cpu_ptr(headp);
  t- >next = NULL;
  *head- >tail = t;
  head- >tail = &(t- >next);
  raise_softirq_irqoff(softirq_nr);//置起TASKLET_SOFTIRQ,在软中断会触发对应的action
  local_irq_restore(flags);
}
//置起对应类型的softirq为pending状态
void __raise_softirq_irqoff(unsigned int nr)
{
  trace_softirq_raise(nr);
  or_softirq_pending(1UL < < nr);
}

总结

软中断(SoftIrq)使用广泛, 在网络包收发,tasklet 等一些基础功能的实现上都有在广泛使用, 深入了解会对我们更深刻的认识这些基础功能以及避坑有很大的帮助.

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分