linux arm32中断子系统学习总结(四)--- 软中断
四、linux软中断实现机制
软中断是一种内核机制,又叫做中断的“底半部”;内核定义了10种软中断,从程序实现上看,软中断就是一组函数,一种软中断一个函数,只不过内核设计了一种机制来调用这些函数;我们在使用软中断的时候,只要把我们的函数挂在这些软中断的执行函数里面,那么,内核就会通过软中断运行机制调用我们的函数;软中断执行时,有可能处于硬件中断上下文,因此软中断执行函数中不能有引起任务切换的函数;
本文通过以下几个方面来总结软中断的实现机制:
1、内核通过哪些数据结构来组织软中断
2、软中断是在什么时候被运行的
3、以设备驱动常用的tasklet为例,来说明软中断的使用方法
4.1 内核通过哪些数据结构来组织软中断
linux内核中定义了10种软中断,把各软中断的执行函数放在softirq_vec[NR_SOFTIRQS]数组里面;然后,定义了一个数组irq_stat[NR_CPUS],每个数组的元素由一个cpu使用,里面是软中断的pending标志位,表示该cpu有哪些软中断待处理,当需要调度某个软中断运行的时候,只需要把对应的pendging标志位置1,那么当内核处理软中断时,检测pendging标志位,就会执行该软中断的函数。
linux4.14内核中定义了10种软中断,如下的枚举成员表示10种软中断的类型:
enum { HI_SOFTIRQ=0, TIMER_SOFTIRQ, NET_TX_SOFTIRQ, NET_RX_SOFTIRQ, BLOCK_SOFTIRQ, IRQ_POLL_SOFTIRQ, TASKLET_SOFTIRQ, 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 };
内核定义了一个softirq_action结构体数组,每个数组元素表示一种软中断,里面挂接了各软中断的执行函数,数组下标就是各软中断的枚举值,由于内核在执行软中处理函数时是按数组下标遍历如下数组执行,因此,数组下标越小表示软中断的优先级越高,也就是上述软中断枚举中,枚举值越小,表示优先级越高,软中断中优先级最高的是HI_SOFTIRQ
struct softirq_action { void (*action)(struct softirq_action *); }; /* 存放各软中断执行函数的数组 */ static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;
内核定义了一个irq_stat[NR_CPUS]结构体数组,每个数组元素由一个cpu使用,里面有该cpu软中断的pengding标志位__softirq_pending,用于表示该cpu有哪些软中断待处理,当需要让某个cpu执行某个软中断时,只需要将该cpu的软中断pengding标志位置1就可以了。
typedef struct { unsigned int __softirq_pending; unsigned int ipi_irqs[NR_IPI]; } ____cacheline_aligned irq_cpustat_t; /* 表示各cpu软中断pengding标志位的数组 */ irq_cpustat_t irq_stat[NR_CPUS] ____cacheline_aligned;
从上面的数据结构可以看出,各软中断的处理函数是各cpu共享的,但是每个cpu有各自的软中断pending标志位,内核中软中断激活接口也是激活某个cpu的软中断,于是软中断的处理函数需要是可重入的,但是两个用于实现tasklet的软中断例外(HI_SOFTIRQ、TASKLET_SOFTIRQ),内核对这两个tasklet的软中断执行流程做了特殊处理,保证同一时刻只有一个cpu在执行同一个tasklet,当然,不同的tasklet可以并发在不同的cpu执行。
与软中断相关的关键数据结构还有task_struct结构体中的thread_info.preempt_count,preempt_count字段用于记录当前任务所处的context状态;
PREEMPT_BITS[0:7]:该字段记录显示禁用本地cpu内核抢占的次数(怎么禁用本地cpu内核抢占); 用于记录禁止抢占的次数,禁止抢占一次该值就加1,使能抢占该值就减1; SOFTIRQ_BITS[8:15]:软中断计数器,表示可延迟函数被禁用的程度,local_bh_disable()会增加该字段的值 ;用于同步处理,关掉下半部的时候加1,打开下半部的时候减1; HARDIRQ_BITS[16:27]:硬中断计数,表示本地cpu上中断处理程序的嵌套数(irq_enter宏递增它的值,irq_exit()宏递减它的值) ;用于表示处于硬件中断上下文中; PREEMPT_ACTIVE标志[28]:
4.2 软中断是在什么时候被运行的
如下3个点会运行软中断,还有其他的运行点,这里没有一一列出:
1、硬件中断退出函数irq_exit()里面会运行软中断处理函数
2、软中断使能函数local_bh_enable()里面会运行软中断处理函数
3、软中断的内核线程ksoftirqd会运行软中断的处理函数 --- 这个内核线程的优先级比较低,可以保证其他线程有机会运行
这3个运行点都是调用__do_softirq函数来执行softirq_vec数组里面的软中断函数,具体内容如下:
/* * __do_softirq * 运行本cpu处于pending状态的软中断执行函数 * 本函数设计了一个restart的大循环,每次大循环都会读取本cpu的pending标志位 * restart开始运行时,使能cpu本地中断,因此在处理软中断过程中可能会有新的pending标志位被置上 * 然后,在restart循环里面设计了一个while循环,while循环中会调用所有处于pending的软中断执行函数 * while循环处理完所有pengding里面的软中断后,会检查如下条件,看是否再次进行restart大循环 * 1. 再次读取pending标志位,如果没有待处理的软中断,则退出restart * 2. 本次__do_softirq函数运行的时间已经超过MAX_SOFTIRQ_TIME,则退出restart * 3. 当前线程需要被调度出去need_resched,则退出restart * 4. while循环已经进行了MAX_SOFTIRQ_RESTART次,则退出restart * 如果while循环结束后,再次读取的pending标志位里面有新的待处理软中断,但是满足restart退出条件,那么调度软中断线程,退出restart */ __do_softirq /* 取出软中断pending */ pending = local_softirq_pending(); /* * 禁止软中断 * 软中断必须以串行的方式在cpu运行,如果该cpu此时有硬件中断,那么硬中断退出时也会调用软中断处理函数 * 禁止软中断后,in_interrupt()函数会返回1 */ __local_bh_disable_ip(_RET_IP_, SOFTIRQ_OFFSET); preempt_count_add(cnt); /* * 这里会循环执行,直到满足条件才退出循环restart * 退出条件1:本cpu的软中断pending标志位没有待处理的软中断 * 退出条件2:本次软中断处理时间超时MAX_SOFTIRQ_TIME * 退出条件3:当前线程不需要被调度出去need_resched * 退出条件4:处理了MAX_SOFTIRQ_RESTART软中断,每次处理pengding里面所有的标志位 */ restart: /* 处理软中断回调函数前,使能本地cpu硬件中断 */ local_irq_enable(); /* 各软中断回调函数的数组首地址 */ h = softirq_vec; /* * 取出待处理优先级最高的软中断 * 按优先级处理pengding的软中断 * */ while ((softirq_bit = ffs(pending))) unsigned int vec_nr; int prev_count; /* 本次处理的软中断 */ h += softirq_bit - 1; /* * 不懂,这是干嘛 */ vec_nr = h - softirq_vec; kstat_incr_softirqs_this_cpu(vec_nr); /* * 跟踪preempt_count标志位 * 处理软中断回调函数的过程中,如果该标志位发生变化,会打印log提示 */ prev_count = preempt_count(); --- 抢占标志位在哪些情况下会发生变化 /* * 调用软中断的回调函数 */ trace_softirq_entry(vec_nr); h->action(h); trace_softirq_exit(vec_nr); /* * 如果preempt_count标志位发生变化,打印提示 */ 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; /* 至此,完成一次pending标志位的循环处理 */ /* * RCU软中断处理,不懂。。 */ rcu_bh_qs(); /* * 禁止cpu本地硬件中断 */ local_irq_disable(); /* * 读取pending标志位,并判断是否继续循环处理软中断 */ pending = local_softirq_pending(); if (pending) { if (time_before(jiffies, end) && !need_resched() && --max_restart) goto restart; wakeup_softirqd(); } /* 以上为软中断循环处理逻辑 */ /* * 以下为软中断处理函数__do_softirq退出时的处理 */ /* 标志位处理 */ lockdep_softirq_end(in_hardirq); /* * 使能软中断,与上面的__local_bh_disable_ip对应 */ __local_bh_enable(SOFTIRQ_OFFSET); preempt_count_sub(cnt)
4.2.1 硬件中断退出函数irq_exit()里面运行软中断处理函数
在硬件中断处理函数退出的irq_exit函数里面会执行软中断,如下
irq_exit /* 禁用本地中断 */ local_irq_disable(); /* 递减preempt_count字段的硬中断计数器 */ preempt_count_sub(HARDIRQ_OFFSET); /* * 不在硬件中断上下文、没有禁用软中断、并且有软中断待处理,那么开始处理软中断 */ if (!in_interrupt() && local_softirq_pending()) invoke_softirq /* * 如果软中断内核线程ksoftirq正在执行,则直接返回 */ if (ksoftirqd_running()) return; /* * 如果中断强制线程化的全局变量被置1,那么转去调度ksoftirq来执行软中断 * 如果没有强制中断线程化,那么开始处理软中断 */ if (!force_irqthreads) /* * 中断没有强制线程化 * __do_softirq这个函数里面最多直接处理10个软中断,剩余的软中断通过调度ksoftirq线程去处理 */ __do_softirq else /* * 中断强制线程化,那么调度ksoftirq线程去处理软中断 */ wakeup_softirqd
4.2.2 软中断使能函数local_bh_enable()里面运行软中断处理函数
local_bh_enable __local_bh_enable_ip(_THIS_IP_, SOFTIRQ_DISABLE_OFFSET); /* * 禁止本地cpu中断 */ #ifdef CONFIG_TRACE_IRQFLAGS local_irq_disable(); #endif /* * 如果preempt_count里面的软中断字段为SOFTIRQ_DISABLE_OFFSET */ if (softirq_count() == SOFTIRQ_DISABLE_OFFSET) trace_softirqs_on(ip) --- 会使能软中断,但不清楚具体干嘛 /* * 调用软中断回调函数前, * 把preempt_count中软中断字段减去SOFTIRQ_DISABLE_OFFSET - 1 */ preempt_count_sub(cnt - 1); /* * 执行软中断回调函数 */ if (unlikely(!in_interrupt() && local_softirq_pending())) do_softirq(); /* 在硬件中断上下文,或者禁止软中断,直接返回 */ if (in_interrupt()) return; /* * 又一次禁止cpu本地中断 */ local_irq_save(flags); pending = local_softirq_pending(); /* * 如果有待处理的软中断,并且软中断线程没有在执行,那么调用软中断处理函数执行 */ if (pending && !ksoftirqd_running()) do_softirq_own_stack(); __do_softirq(); /* * 使能cpu本地硬件中断 */ local_irq_restore(flags); /* * 处理完软中断回调函数后,再把preempt_count中软中断字段减去1 */ preempt_count_dec(); /* * 使能cpu本地硬件中断 */ #ifdef CONFIG_TRACE_IRQFLAGS local_irq_enable(); #endif /* * 按条件是否执行调度 */ preempt_check_resched(); if (should_resched(0)) __preempt_schedule();
4.2.3 软中断的内核线程ksoftirqd中运行软中断的处理函数
每个cpu都有一个软中断线程
/* * 内核线程ksoftirqd创建流程 */ static struct smp_hotplug_thread softirq_threads = { .store = &ksoftirqd, .thread_should_run = ksoftirqd_should_run, .thread_fn = run_ksoftirqd, .thread_comm = "ksoftirqd/%u", }; static __init int spawn_ksoftirqd(void) { cpuhp_setup_state_nocalls(CPUHP_SOFTIRQ_DEAD, "softirq:dead", NULL, takeover_tasklets); BUG_ON(smpboot_register_percpu_thread(&softirq_threads)); return 0; } early_initcall(spawn_ksoftirqd); /* * 软中断的内核线程ksoftirqd中,软中断的执行流程 * ksoftirqd回调函数run_ksoftirqd * 哪个cpu激活的软中断,那么这个软中断就由那个cpu执行 */ run_ksoftirqd /* 禁止cpu本地中断 */ local_irq_disable(); /* 如果有pending的软中断,则进行处理 */ if (local_softirq_pending()) __do_softirq(); --- irq_exit、local_bh_enable函数中都调用这个函数处理软中断 /* 使能cpu本地中断 */ local_irq_enable();
4.3 tasklet使用方法
4.3.1 tasklet初始化
/* * tasklet初始化 * 初始化只需要定义一个结构体变量struct tasklet_struct * 然后,使用tasklet_init函数,将tasklet的执行函数绑定到结构体变量struct tasklet_struct */ void tasklet_init(struct tasklet_struct *t, void (*func)(unsigned long), unsigned long data) { t->next = NULL; t->state = 0; atomic_set(&t->count, 0); t->func = func; t->data = data; }
4.3.2 tasklet挂接
tasklet可以挂接在两个软中断中运行:HI_SOFTIRQ:优先级最高的软中断、TASKLET_SOFTIRQ:优先级较低的软中断;
tasklet_hi_schedule接口用来将tasklet绑定到软中断HI_SOFTIRQ;
/* * tasklet_hi_schedule * 函数执行内容如下 */ tasklet_hi_schedule /* 如果这个tasklet正在被调度运行,那么,直接返回,否则,将tasklet挂接到优先级最高的软中断 */ if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state)) __tasklet_hi_schedule(t); /* 禁止cpu本地硬件中断 */ local_irq_save(flags); t->next = NULL; /* 将tasklet挂接到本cpu的HI_SOFTIRQ软中断对应的链表尾部 */ *__this_cpu_read(tasklet_hi_vec.tail) = t; /* 将链表尾指针置空 */ __this_cpu_write(tasklet_hi_vec.tail, &(t->next)); /* * 将本cpu的软中断pending标志位的HI_SOFTIRQ位置1 * 那么在上面所述的3个软中断运行点中,就会执行HI_SOFTIRQ软中断的回调函数,从而调用到本tasklet的执行函数 */ raise_softirq_irqoff(HI_SOFTIRQ); __raise_softirq_irqoff(nr); or_softirq_pending(1UL << nr); /* 如果不在硬件中断上下文,并且软中断没有被置位,那么唤醒软中断线程运行 */ if (!in_interrupt()) wakeup_softirqd(); struct task_struct *tsk = __this_cpu_read(ksoftirqd); if (tsk && tsk->state != TASK_RUNNING) wake_up_process(tsk); /* 使能cpu本地中断 */ local_irq_restore(flags);
tasklet_schedule接口用来将tasklet绑定到软中断TASKLET_SOFTIRQ;
/* * tasklet_schedule * 函数执行内容如下 * 执行流程与tasklet_hi_schedule一致,只是将tasklet与TASKLET_SOFTIRQ软中断绑定 */ static inline void tasklet_schedule(struct tasklet_struct *t) if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state)) __tasklet_schedule(t); local_irq_save(flags); t->next = NULL; *__this_cpu_read(tasklet_vec.tail) = t; __this_cpu_write(tasklet_vec.tail, &(t->next)); raise_softirq_irqoff(TASKLET_SOFTIRQ); local_irq_restore(flags);
4.3.3 tasklet执行函数被调用的流程
tasklet的执行函数由软中断TASKLET_SOFTIRQ或HI_SOFTIRQ的执行函数调用执行;TASKLET_SOFTIRQ或HI_SOFTIRQ的执行函数在软中断初始化时绑定,
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; } open_softirq(TASKLET_SOFTIRQ, tasklet_action); open_softirq(HI_SOFTIRQ, tasklet_hi_action); }
tasklet执行函数被调用流程如下,从执行流程可以看出,tasklet使用了TASKLET_STATE_SCHED做判断,使得同一个tasklet不能并发在不同cpu执行,因此,tasklet的执行函数不需要是可重入函数;
/* * tasklet执行函数被调用的流程 * HI_SOFTIRQ * TASKLET_SOFTIRQ软中断的执行流程与HI_SOFTIRQ一样 */ __do_softirq /* * 禁止软中断,使能本地cpu硬件中断 */ __local_bh_disable_ip(_RET_IP_, SOFTIRQ_OFFSET); local_irq_enable(); /* * 调用各软中断的action函数 * HI_SOFTIRQ对应的action函数是:tasklet_hi_action */ h->action(h); --- 也就是:tasklet_hi_action /* tasklet_hi_action */ /* * 把tasklet_struct链表从软中断的数组中拷贝出来,并且把软中断中的链表置空 * 为什么这个操作需要关中断进行?因为只有硬件中断会打断软中断,然后,操作软中断链表 */ local_irq_disable(); list = __this_cpu_read(tasklet_hi_vec.head); __this_cpu_write(tasklet_hi_vec.head, NULL); __this_cpu_write(tasklet_hi_vec.tail, this_cpu_ptr(&tasklet_hi_vec.head)); local_irq_enable(); /* * 遍历运行链表中的所有tasklet */ while (list) /* * 通过标志位TASKLET_STATE_SCHED判断本tasklet有没有在其他cpu执行 * 如果在其他cpu执行,那么不执行tasklet的执行函数,将本tasklet再放回软中断的链表,等下次软中断被调度时再执行 */ if (tasklet_trylock(t)) { t->func(t->data); --- tasklet的执行函数 tasklet_unlock(t); continue; } tasklet_unlock(t); /* 将tasklet重新放回软中断的链表 */ local_irq_disable(); t->next = NULL; *__this_cpu_read(tasklet_hi_vec.tail) = t; __this_cpu_write(tasklet_hi_vec.tail, &(t->next)); __raise_softirq_irqoff(HI_SOFTIRQ); local_irq_enable(); /* * 使能软中断,禁止本地cpu硬件中断 */ local_irq_disable(); __local_bh_enable(SOFTIRQ_OFFSET);
参考资料:
《深入理解linux内核》
https://www.cnblogs.com/LoyenWang/p/13124803.html