本文摘抄于奔跑吧linux内核:基于linux内核源码问题分析
中断上半部:硬件中断处理程序以在关中断的情况下进行(关闭本CPU的所有中断响应,arm是处理器自动完成的)。中断上半部一般完成中断处理的一小部分。eg 响应中断已经被软件接收、硬件中断处理完成时,发送EOI信号给中断控制器
中断下半部——SoftIRQ
软中断是预留给系统中对时间要求最严格和最重要的下半部使用。
对时间要求最严格:应该是被此中断上半部退出的时候都会去尝试执行软中断。因此是下半部里面最快的(软中断,tasklet,工作队列,中断线程化)
软中断类型
/* 通过枚举类型声明软中断,且索引越小,优先级更高 */
enum
{
HI_SOFTIRQ=0,//优先级为0,是最高优先级的软中断
TIMER_SOFTIRQ,//定时器的软中断
NET_TX_SOFTIRQ,//发生网络数据包的软中断
NET_RX_SOFTIRQ,//收包的软中断
BLOCK_SOFTIRQ,//块设备的软中断
BLOCK_IOPOLL_SOFTIRQ,//块设备的软中断
TASKLET_SOFTIRQ,//专门给tasklet机制准备的软中断,难怪说tasklet基于软中断实现
SCHED_SOFTIRQ,//进程调度以及负载均衡
HRTIMER_SOFTIRQ,//高精度定时器
RCU_SOFTIRQ, //为RCU服务的软中断 /* Preferable RCU should always be the last softirq */
NR_SOFTIRQS
};
描述软中断的数据结构。当触发了软中断,就会调用action回调函数处理软中断
struct softirq_action
{
void (*action)(struct softirq_action *);
};
软中断描述符数组。从软中断注册函数open_softirq看,感觉软中断只有10个,
static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;
结构体irq_cpustat用于描述软中断的状态信息。同时定义了一个数组irq_stat[NR_CPUS],这样每个CPU有一个独立的软中断状态信息
在软中断状态信息irq_cpustat_t成员__softirq_pending用于表示有软中断待处理。从代码看的话,__softirq_pending的低10位就分别对应了10种软中断。例如__softirq_pending的bit 0置位表示有HI_SOFTIRQ软中断待处理。同理,我们想触发软中断也是将对应bit位置上,实现的。
typedef struct {
unsigned int __softirq_pending;
#ifdef CONFIG_SMP
unsigned int ipi_irqs[NR_IPI];
#endif
} ____cacheline_aligned irq_cpustat_t;
irq_cpustat_t irq_stat[NR_CPUS] ____cacheline_aligned;
软中断注册
/*
nr是软中断的序号*(上面的9个)
*/
void open_softirq(int nr, void (*action)(struct softirq_action *))
{
softirq_vec[nr].action = action;
}
软中断执行时机:1主动触发软中断 2 硬件中断处理(中断上半部)结束时 3 local_bh_enable
1主动触发软中断(准确是ksoftirq内核线程中)
raise_softirq会关闭本地CPU中断,raise_softirq_irqoff则不会。因此后者可以用在进程上下文。为什么啊?难道前者不可以??不理解。。。
另外的书里面是这样说的:如果中断本来已经禁止了,那么可以调用raise_softirq_irqoff,这样会带来一些优化效果。
我更偏向于这种说法。两者的区别只是重复关开了本cpu的中断。而关中断也执行重复的设置了寄存器。
其实从代码来看,这个主动触发软中断,也并不会马上去执行软中断。只有不在中断上下文时,才会去唤醒软中断线程去执行软中断(其实这个好像也只是将专门负责执行软中断的内核线程设置为run状态,并加入到就绪队列里面,并不会真正马上执行。没有去验证过),否则只是打了一个软中断待执行的标记
void raise_softirq(unsigned int nr)
{
unsigned long flags;
local_irq_save(flags);
raise_softirq_irqoff(nr);
local_irq_restore(flags);
}
inline void raise_softirq_irqoff(unsigned int nr)
{
/*
__raise_softirq_irqoff(nr)会将本CPU的irq_stat中的__softirq_pending(软中断状态寄存器)
对应的bit置1
中断返回时,会检查__softirq_pending,如果不为0,则说明有pending的软中断需要处理
*/
__raise_softirq_irqoff(nr);
if (!in_interrupt())
wakeup_softirqd();
}
void __raise_softirq_irqoff(unsigned int nr)
{
trace_softirq_raise(nr);
or_softirq_pending(1UL << nr);
}
#define or_softirq_pending(x) (local_softirq_pending() |= (x))
__raise_softirq_irqoff(nr)会将本CPU的irq_stat中的__softirq_pending(软中断状态寄存器)
对应的bit置1。 in_interrupt()。如果当前不在硬中断上下文或者软中断上下文(其实就是不在中断上下文),那么就去唤醒内核线程ksoftirqd处理软中断。
1、可以看到ksoftirqd是一个per-CP的,因此每个CPU都有一个单独的ksoftirqd内核线程,用于执行本地CPU的中断。
2、在run_ksoftirqd里面是先屏蔽中断了的。但是__do_softirq实际上执行软中断的过程中又开启了本CPU的硬件中断。不知道为什么这样做。。。
3、最终在__do_softirq中去循环遍历本CPU的软中断状态信息__softirq_pending,然后调用相应的处理函数处理软中断
感觉如果进入了run_softirq中,此时应该是处于进程上下文,又能够被硬件中断或者软中断打断(然后软中断执行的条件保证了,在同一个cpu上是串行执行的)
还有就是在当软中断重复执行10次也仍还存在软中断,那么也会调用wakeup_softirqd。这样能够避免软中断太多,导致长期处于中断上下文,使得普通进程得不到执行??
但是该内核线程虽然运行在进程上下文,但是并不允许睡眠,原因见文末
smpboot_register_percpu_thread为每个cpu都创建了一个内核线程用于执行软中断
DECLARE_PER_CPU(struct task_struct *, 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)
{
register_cpu_notifier(&cpu_nfb);
BUG_ON(smpboot_register_percpu_thread(&softirq_threads));
return 0;
}
static void run_ksoftirqd(unsigned int cpu)
{
local_irq_disable();
if (local_softirq_pending()) {
/*
* We can safely run softirq on inline stack, as we are not deep
* in the task stack here.
*/
__do_softirq();
rcu_note_context_switch(cpu);
local_irq_enable();
cond_resched();//软中断执行完毕之后,主动让出CPU,进行休眠
return;
}
local_irq_enable();
}
软中断处理的内核线程唤醒(如果没有被其他人唤醒。它自己应该也能被调度运行。只不过该内核线程的优先级比较低,主要是为了防止耽误了其他线程的运行)
static void wakeup_softirqd(void)
{
/* Interrupts are disabled: no need to stop preemption */
struct task_struct *tsk = __this_cpu_read(ksoftirqd);
if (tsk && tsk->state != TASK_RUNNING)
wake_up_process(tsk);
}
2 硬件中断处理(中断上半部)结束时
void handle_IRQ(unsigned int irq, struct pt_regs *regs)
{
struct pt_regs *old_regs = set_irq_regs(regs);
irq_enter();
if (unlikely(irq >= nr_irqs)) {
if (printk_ratelimit())
printk(KERN_WARNING "Bad IRQ%u\n", irq);
ack_bad_irq(irq);
} else {
generic_handle_irq(irq);
}
irq_exit();
set_irq_regs(old_regs);
}
void irq_exit(void)
{
...................
/* 不在硬件中断上下文、软中断上下文 同时有软中断待处理 */
/*
因此如果在执行软中断的过程中,被硬件中断打断,然后返回时,
会回到软中断上下文。这些不满足上述条件.不会重新调度新的软中断,
即不会执行invoke_softirq
因此软中断在一个CPU上总是串行执行的
*/
if (!in_interrupt() && local_softirq_pending())
invoke_softirq();
.....................
}
硬件中断处理退出是,irq_exit会检查当前是否有待处理(pendiing)的软中断.
local_softirq_pending()检查本地CPU的"软中断状态寄存器" __softirq_pending是否有需要处理的软中断
in_interrupt:是否在硬件中断上下文或者软中断上下文。因此如果在执行软中断的过程中,被硬件中断打断,然后返回时,会回到软中断上下文。这些不满足上述条件.不会重新调度新的软中断, 即不会执行invoke_softirq。 因此软中断在一个CPU上总是串行执行的
从__do_softirq中可以看出:
1、在软中断的执行是开中断的 ,因此在软中断执行过程中又可以被硬中断打断。在该函数中循环遍历软中断状态信息的各个bit位。然后去调用相应的软中断处理函数。
2、在函数进来的时候使用__local_bh_disable_ip,去修改preempt_count。禁用了中断下半部,或者说表明在软中断上下文。这样即使开中断被打断,当中断上半部退出irq_exit时,in_interrupt里面去检查preempt_count时为非0,导致无法执行新的软中断。只能继续执行被打断的软中断。软中断在一个cpu上串行执行,是由这两个地方保证的。
另外软中断也不会一直处理。从代码中可以看到如果循环超过10或者是执行时间超过2ms,则会唤醒ksoftirq/n内核线程去处理。因为如果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;
//获取本地CPU的"软中断状态寄存器"
pending = local_softirq_pending();
account_irq_enter_time(current);
/*
增加preempt_count中SOFTIRQ域的计数,表明现在是在软中断上下文
屏蔽所有软中断,也证明每个cpu上运行的软中断只有一个
*/
__local_bh_disable(_RET_IP_, SOFTIRQ_OFFSET);
//进入软中断
lockdep_softirq_enter();
cpu = smp_processor_id();
restart:
/* Reset the pending bitmask before enabling irqs */
set_softirq_pending(0);//清除软中断寄存器,因为后面会依次处理所有的软中断
/*
开启硬件中断,执行到这里以后,软中断就能够被硬件中断抢占了
在这之前硬件中断都是被屏蔽了的
注意:虽然这里打开了硬件中断,但是上面代码__local_bh_disable将软中断屏蔽了
因此即使先代码被硬中断打断,在退出硬件中断时,由于in_interrupt不满足,只能回到该软中断
被打断的地方继续执行
*/
local_irq_enable();
h = softirq_vec;
/* 循环处理所有的软中断.总共9个 */
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);
h->action(h);
trace_softirq_exit(vec_nr);
/* 这只能说明软中断处理函数里面preempt_count不对称的情况 */
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_set(prev_count);
}
rcu_bh_qs(cpu);
}
h++;
pending >>= 1;
} while (pending);
/*
到这里软中断的处理函数以及执行完毕.
但是由于在软中断执行前,开了本地CPU的中断。
因此在这段时间可能会发送硬中断以及再次触发软中断.
重新再去检查一下软中断状态寄存器
*/
local_irq_disable();
/*
上面提到由于__local_bh_disable屏蔽了软中断,即使被硬件中断打断,也无法重新进入该函数
只得回到该函数被打断的地方继续执行。但是由于软中断触发相当于都是修改的per cpu变量。
因此相当于在这里给了新触发的软中断一个指向的机会
*/
pending = local_softirq_pending();
if (pending) {
/*
当执行软中断的过程中再次触发软中断。重新执行软中断的条件
1、软中断处理时间没有超过2ms
2、当前进程没有调度需求
3、循环处理的次数不能超过max_restart(10)次
*/
if (time_before(jiffies, end) && !need_resched() &&
--max_restart)
goto restart;
wakeup_softirqd();
}
/* 退出软中断上下文 */
lockdep_softirq_exit();
account_irq_exit_time(current);
/*
清除表示处于软中断上下文的标记
与之相对的是上面的__local_bh_disable(_RET_IP_, SOFTIRQ_OFFSET)
*/
__local_bh_enable(SOFTIRQ_OFFSET);
WARN_ON_ONCE(in_interrupt());
tsk_restore_flags(current, old_flags, PF_MEMALLOC);
}
3 local_bh_enable()
可以看到local_bh_enble实现里面,如果不在中断上下文,并且有待处理的软中断,也会去执行软中断do_softirq();
可能是担心软中断被屏蔽太久了,因此在使能软中断的时候,检查一下是否需要执行软中断
static inline void local_bh_enable(void)
{
__local_bh_enable_ip(_THIS_IP_, SOFTIRQ_DISABLE_OFFSET);
}
void __local_bh_enable_ip(unsigned long ip, unsigned int cnt)
{
WARN_ON_ONCE(in_irq() || irqs_disabled());
................................
preempt_count_sub(cnt - 1);
if (unlikely(!in_interrupt() && local_softirq_pending())) {
/*
* Run softirq if any pending. And do it in its own stack
* as we may be calling this deep in a task call stack already.
*/
do_softirq();
}
..................................
}
因此硬中断会抢占软中断,软中断又会抢占进程和线程
之前在想spin_lock_irq会屏蔽软中断不?现在看来是不能屏蔽软中断的。因为软中断有两个途径执行。屏蔽中断只是使得软中断无法从中断上半部退出时被执行
执行被local_bh_disable()和local_bh_enable()包围的代码区域时,由于softirq是被屏蔽的,因而在这段时间里也是不能睡眠的。难道是因为,睡眠切到了其他进程,然后直到切回来这段时间,都无法响应软中断??
从local_bh_disable/local_bh_enable的代码实现,并不是因为进程切换走到切回来这段时间无法响应软中断,才不能切换。上述的两个函数起始只是针对thread_info->preempt_count进行修改。thread_info是每个进程或者内核线程独有的。被切走的线程preempt_count被local_bh_disable修改了,并不代表。即将运行的进程的preempt_count也处于中断上下文(in_interrupt()非0),无法执行软中断。
《linux内核设计与实现》关于内核抢占有这样一种说法,preempt_count计数器初值为0,获取锁+1,释放锁-1。当计数为0说明内核可执行抢占,即调度。反之不为0,说明当前进程或者线程仍持有锁,进行调度是不安全的。
为什么在ksoftirqd线程执行期间也不允许睡眠?因为进入ksoftirqd之后,softirq也是被屏蔽的,相当于是执行了local_bh_disable()。因此进入softirq是在softirq上下文,关闭softirq抢占也是在softirq上下文
另外本CPU的软中断状态信息__softirq_pending这里面待执行软中断标记又是何时被打上的呢?在中断处理程序中触发软中断是最常见的形式。
中断上半部_这个我好像学过的博客-CSDN博客_irq_svc文末写了一个例子
/* 定义 taselet */
struct tasklet_struct testtasklet;
/* tasklet 处理函数 */
void testtasklet_func(unsigned long data)
{
/* tasklet 具体处理内容 */
}
/* 中断处理函数 */
irqreturn_t test_handler(int irq, void *dev_id)
{
......
/* 调度 tasklet */
tasklet_schedule(&testtasklet);
......
}
/* 驱动入口函数 */
static int __init xxxx_init(void)
{
......
/* 初始化 tasklet */
tasklet_init(&testtasklet, testtasklet_func, data);
/* 注册中断处理函数 */
request_irq(xxx_irq, test_handler, 0, "xxx", &xxx_dev);
......
}
其实就是我们通过request_irq(xxx_irq, test_handler, 0, "xxx", &xxx_dev);注册的回调函数去设置软中断的标记的。这个注册的回调函数test_handler其实是属于中断上半部的流程。
可以看到test_handler里面调用了tasklet_schedule,而tasklet_schedule里面就去使用raise_softirq_irqoff主动触发了软中断。相当于就是中断上半部就会设置软中断的状态信息
void __tasklet_schedule(struct tasklet_struct *t)
{
unsigned long flags;
...............
raise_softirq_irqoff(TASKLET_SOFTIRQ);
.................
}