作为软中断,从内核同步的角度来说它有两个特点:一是软中断总是和cpu绑定在一起的,二是除了中断或是异常(一般内核太不会出现异常)没有什么东西能够抢占它。因为和cpu绑定,软中断喜欢使用cpu变量,这样就不用考虑SMP的竞争,因为不会被其他软中断或是内核抢占,使得不用在嵌套上太过于小心。一般而言,软中断是在中断的下半部分执行的,优先级大于进程,不过大量的软中断会阻塞进程的正常进行。因此内核有一个机制,软中断如果连续出现多次后就不再继续在中断下半部分执行软中断,而是将其放到ksoftirqd内核线程中继续执行。
点击(此处)折叠或打开
asmlinkage void __do_softirq(void)
{
....
intmax_restart=MAX_SOFTIRQ_RESTART;
....
pending=local_softirq_pending();
if(pending&&--max_restart)
goto restart;
if(pending)
wakeup_softirqd();
....
} 《深入理解linux内核》提到,内核线程优先级较低,当系统负荷较低的时候,就会调度该线程继续完成软中断。每一个cpu对应一个 ksofti rqd内核线程 ,但是作为内核线程, ksofti rqd与中断的下半部分的执行环境是非常不一样的,那它是如何能够 完成软中断呢。
首先先说说绑定cpu的问题,由于启动软中断是通过设置的是cpu变量__softirq_pending来通知cpu的。一旦这个cpu中断了,在中断的下半部分里会检查此部分,若检查到有软中断请求就执行软中断。因此可以说向在哪个cpu上申请了软中断,就会在那个cpu上执行软中断。但是内核线程不能保证这点,就拿工作队列work thread而言,虽然说是每个cpu拥有一个work队列,但对于的执行内核线程worker_thread却不是cpu独有的。比如我向3号cpu上的队列里提交了一个work,有可能这个work是在1号cpu里运行完成的。为什么会这样呢,是因为内核线程是可以被抢占(开启内核抢占)或是主动休眠的。一旦内核线程不是正在当前cpu正在运行的进程,cpu在调度的时候就有可能将其放到别的cpu上面。内核之所以这样做是处于平衡各个cpu工作量的考虑,但这样就给我们 ksofti rq d执行软中断带来了麻烦。因为ksoftirqd是不可能一直霸占着cpu不放的,事实上在代码中ksoftirqd会检查TIF_NEED_RESCHED位来主动放弃cpu。那内核是如何解决这个问题的呢,其实很简单,只要内核将ksoftirqd绑定在对应的cpu运行队列上就行了。
点击(此处)折叠或打开
p=kthread_create(ksoftirqd,hcpu,"ksoftirqd/%d",hotcpu);
if(IS_ERR(p)){
printk("ksoftirqd for %i failed\n",hotcpu);
return NOTIFY_BAD;
}
kthread_bind(p,hotcpu); 在创建 ksoftirqd内核线程之后就会调kthread_bind来绑定cpu。这样在内核调度的时候就会将ksoftirqd放在对应cpu的运行队列里,ksoftirqd必定运行在对应cpu上的效果。
点击(此处)折叠或打开
void kthread_bind(struct task_struct*k,unsignedintcpu)
{
/*Must have done schedule()inkthread()before we set_task_cpu*/
if(!wait_task_inactive(k,TASK_UNINTERRUPTIBLE)){
WARN_ON(1);
return;
}
set_task_cpu(k,cpu);
k->cpus_allowed=cpumask_of_cpu(cpu);
k->rt.nr_cpus_allowed=1;
k->flags|=PF_THREAD_BOUND;
}
其次再说说软中断的执行函数do_softirq,在do_softirq一开始就禁用了本地中断的。这样主要是防止do_softirq本身的嵌套,如果在do_softirq开始到软中断服务函数执行这段时间内又发生了中断,在中断的下半部分里还会调用do_softirq来处理之前没有来的及处理的软中断,这样造成了竞争的。而一旦到了软中断服务函数开始执行的时候,内核会开启本地中断同时禁用软中断,一方面允许内核抢占,一方面防止软中断的嵌套。
就do_softirq函数本身来说,除了在中断服务函数里调用(请区分中断服务函数和哈中断服务函数执行完后进入的中断的下半部分)之外,是可以在其他内核线程里调用。因为是满足不被软中断和内核抢占的要求,而其处理的软中断请求而也是执行do_softirq的cpu的软中断,在内核线程里使用是没有问题的。我们可以看到在内核里很多内核线程使用local_bh_enable来启用do_softirq的例子。那为什么ksoftirqd要绑定cpu,而其他的使用local_bh_enable的内核线程不需要绑定cpu呢。
要注意,内核线程调local_bh_enable启用do_softirq是一个主动操作。local_bh_enable对应的是local_bh_disable,后者是禁用软中断,前者是重新开启软中断,当开启软中断的时候,会检查是否有软中断请求,一旦有就调用do_softirq,来执行软中断。可以看出,是因为之前关了软中断,现在开的时候发现该cpu有相应的软中断要处理就开始启用处理函数。而从发现cpu上有软中断请求到执行do_softirq里面只有一步,虽然这里没有禁止内核抢占,但内核刚好在这一步被抢占的概率的微乎其微,而且就算被抢占换到了其他cpu,do_softirq也只是完成这个新的cpu的软中断操作,不会影响原来cpu的软中断请求,原来的cpu请求要么通过中断的下半部分,要么通过ksoftirqd内核线程来处理。
点击(此处)折叠或打开
if(unlikely(!in_interrupt()&&local_softirq_pending()))
do_softirq();
但是向cpu发出请求到唤醒ksoftirqd内核线程的时间是很长的,而如果这个ksoftirqd不是运行在要请求的cpu上的话,那么无论执行do_softirq与否,都不会解决原来cpu的软中断请求。这样ksoftirqd不能及时完成对于cpu在中断下半部分未能处理完的软中断请求。因此ksoftirqd必须绑定在对应cpu上,这样才能作为中断下半部分的补充,来更好的完成内核的软中断机制。
概述
从本质上来讲,中断是一种电信号,当设备有某种事件发生时,它就会产生中断,通过总线把电信号发送给中断控制器。
如果中断的线是激活的,中断控制器就把电信号发送给处理器的某个特定引脚。处理器于是立即停止自己正在做的事,
跳到中断处理程序的入口点,进行中断处理。
(1) 硬中断
由与系统相连的外设(比如网卡、硬盘)自动产生的。主要是用来通知操作系统系统外设状态的变化。比如当网卡收到数据包
的时候,就会发出一个中断。我们通常所说的中断指的是硬中断(hardirq)。
(2) 软中断
为了满足实时系统的要求,中断处理应该是越快越好。linux为了实现这个特点,当中断发生的时候,硬中断处理那些短时间
就可以完成的工作,而将那些处理事件比较长的工作,放到中断之后来完成,也就是软中断(softirq)来完成。
(3) 中断嵌套
Linux下硬中断是可以嵌套的,但是没有优先级的概念,也就是说任何一个新的中断都可以打断正在执行的中断,但同种中断
除外。软中断不能嵌套,但相同类型的软中断可以在不同CPU上并行执行。
(4) 软中断指令
int是软中断指令。
中断向量表是中断号和中断处理函数地址的对应表。
int n - 触发软中断n。相应的中断处理函数的地址为:中断向量表地址 + 4 * n。
(5)硬中断和软中断的区别
软中断是执行中断指令产生的,而硬中断是由外设引发的。
硬中断的中断号是由中断控制器提供的,软中断的中断号由指令直接指出,无需使用中断控制器。
硬中断是可屏蔽的,软中断不可屏蔽。
硬中断处理程序要确保它能快速地完成任务,这样程序执行时才不会等待较长时间,称为上半部。
软中断处理硬中断未完成的工作,是一种推后执行的机制,属于下半部。
开关
(1) 硬中断的开关
简单禁止和激活当前处理器上的本地中断:
local_irq_disable();
local_irq_enable();
保存本地中断系统状态下的禁止和激活:
unsigned long flags;
local_irq_save(flags);
local_irq_restore(flags);
(2) 软中断的开关
禁止下半部,如softirq、tasklet和workqueue等:
local_bh_disable();
local_bh_enable();
需要注意的是,禁止下半部时仍然可以被硬中断抢占。
(3) 判断中断状态
#define in_interrupt() (irq_count())// 是否处于中断状态(硬中断或软中断)
#define in_irq()(hardirq_count()) // 是否处于硬中断
#define in_softirq() (softirq_count()) // 是否处于软中断
硬中断
(1) 注册中断处理函数
注册中断处理函数:
[java]view plain copy /** *irq:要分配的中断号 *handler:要注册的中断处理函数 *flags:标志(一般为0) *name:设备名(dev->name) *dev:设备(structnet_device*dev),作为中断处理函数的参数 *成功返回0 */ intrequest_irq(unsignedintirq,irq_handler_thandler,unsignedlongflags, constchar*name,void*dev);
中断处理函数本身:
[java]view plain copy typedefirqreturn_t(*irq_handler_t)(int,void*); /** *enumirqreturn *@IRQ_NONE:interruptwasnotfromthisdevice *@IRQ_HANDLED:interruptwashandledbythisdevice *@IRQ_WAKE_THREAD:handlerrequeststowakethehandlerthread */ enumirqreturn{ IRQ_NONE, IRQ_HANDLED, IRQ_WAKE_THREAD, }; typedefenumirqreturnirqreturn_t; #defineIRQ_RETVAL(x)((x)!=IRQ_NONE)
(2) 注销中断处理函数
[java]view plain copy /** *free_irq-freeaninterruptallocatedwithrequest_irq *@irq:Interruptlinetofree *@dev_id:Deviceidentitytofree * *Removeaninterrupthandler.Thehandlerisremovedandifthe *interruptlineisnolongerinusebyanydriveritisdisabled. *OnasharedIRQthecallermustensuretheinterruptisdisabled *onthecarditdrivesbeforecallingthisfunction.Thefunctiondoes *notreturnuntilanyexecutinginterruptsforthisIRQhavecompleted. *Thisfunctionmustnotbecalledfrominterruptcontext. */ voidfree_irq(unsignedintirq,void*dev_id);
软中断
(1) 定义
软中断是一组静态定义的下半部接口,可以在所有处理器上同时执行,即使两个类型相同也可以。
但一个软中断不会抢占另一个软中断,唯一可以抢占软中断的是硬中断。
软中断由softirq_action结构体表示:
[java]view plain copy structsoftirq_action{ void(*action)(structsoftirq_action*);/*软中断的处理函数*/ };
目前已注册的软中断有10种,定义为一个全局数组:
[java]view plain copy staticstructsoftirq_actionsoftirq_vec[NR_SOFTIRQS]; enum{ HI_SOFTIRQ=0,/*优先级高的tasklets*/ TIMER_SOFTIRQ,/*定时器的下半部*/ NET_TX_SOFTIRQ,/*发送网络数据包*/ NET_RX_SOFTIRQ,/*接收网络数据包*/ BLOCK_SOFTIRQ,/*BLOCK装置*/ BLOCK_IOPOLL_SOFTIRQ, TASKLET_SOFTIRQ,/*正常优先级的tasklets*/ SCHED_SOFTIRQ,/*调度程序*/ HRTIMER_SOFTIRQ,/*高分辨率定时器*/ RCU_SOFTIRQ,/*RCU锁定*/ NR_SOFTIRQS/*10*/ };
(2) 注册软中断处理函数
[java]view plain copy /** *@nr:软中断的索引号 *@action:软中断的处理函数 */ voidopen_softirq(intnr,void(*action)(structsoftirq_action*)) { softirq_vec[nr].action=action; }
例如:
open_softirq(NET_TX_SOFTIRQ, net_tx_action);
open_softirq(NET_RX_SOFTIRQ, net_rx_action);
(3) 触发软中断
调用raise_softirq()来触发软中断。
[java]view plain copy voidraise_softirq(unsignedintnr) { unsignedlongflags; local_irq_save(flags); raise_softirq_irqoff(nr); local_irq_restore(flags); } /*Thisfunctionmustrunwithirqsdisabled*/ inlinevoidrasie_softirq_irqsoff(unsignedintnr) { __raise_softirq_irqoff(nr); /*Ifwe'reinaninterruptorsoftirq,we'redone *(thisalsocatchessoftirq-disabledcode).Wewill *actuallyrunthesoftirqoncewereturnfromtheirq *orsoftirq. *Otherwisewewakeupksoftirqdtomakesurewe *schedulethesoftirqsoon. */ if(!in_interrupt())/*如果不处于硬中断或软中断*/ wakeup_softirqd(void);/*唤醒ksoftirqd/n进程*/ }
Percpu变量irq_cpustat_t中的__softirq_pending是等待处理的软中断的位图,通过设置此变量
即可告诉内核该执行哪些软中断。
[java]view plain copy staticinlinevoid__rasie_softirq_irqoff(unsignedintnr) { trace_softirq_raise(nr); or_softirq_pending(1UL<<nr); } typedefstruct{ unsignedint__softirq_pending; unsignedint__nmi_count;/*archdependent*/ }irq_cpustat_t; irq_cpustat_tirq_stat[]; #define__IRQ_STAT(cpu,member)(irq_stat[cpu].member) #defineor_softirq_pending(x)percpu_or(irq_stat.__softirq_pending,(x)) #definelocal_softirq_pending()percpu_read(irq_stat.__softirq_pending)
唤醒ksoftirqd内核线程处理软中断。
[java]view plain copy staticvoidwakeup_softirqd(void) { /*Interruptsaredisabled:noneedtostoppreemption*/ structtask_struct*tsk=__get_cpu_var(ksoftirqd); if(tsk&&tsk->state!=TASK_RUNNING) wake_up_process(tsk); }
在下列地方,待处理的软中断会被检查和执行:
1. 从一个硬件中断代码处返回时
2. 在ksoftirqd内核线程中
3. 在那些显示检查和执行待处理的软中断的代码中,如网络子系统中
而不管是用什么方法唤起,软中断都要在do_softirq()中执行。如果有待处理的软中断,
do_softirq()会循环遍历每一个,调用它们的相应的处理程序。
在中断处理程序中触发软中断是最常见的形式。中断处理程序执行硬件设备的相关操作,
然后触发相应的软中断,最后退出。内核在执行完中断处理程序以后,马上就会调用
do_softirq(),于是软中断开始执行中断处理程序完成剩余的任务。
下面来看下do_softirq()的具体实现。
[java]view plain copy asmlinkagevoiddo_softirq(void) { __u32pending; unsignedlongflags; /*如果当前已处于硬中断或软中断中,直接返回*/ if(in_interrupt()) return; local_irq_save(flags); pending=local_softirq_pending(); if(pending)/*如果有激活的软中断*/ __do_softirq();/*处理函数*/ local_irq_restore(flags); }[java]view plain copy /*WerestartsoftirqprocessingMAX_SOFTIRQ_RESTARTtimes, *andwefallbacktosoftirqdafterthat. *Thisnumberhasbeenestablishedviaexperimentation. *Thetwothingstobalanceislatencyagainstfairness-wewant *tohandlesoftirqsassoonaspossible,buttheyshouldnotbe *abletolockupthebox. */ asmlinkagevoid__do_softirq(void) { structsoftirq_action*h; __u32pending; /*本函数能重复触发执行的次数,防止占用过多的cpu时间*/ intmax_restart=MAX_SOFTIRQ_RESTART; intcpu; pending=local_softirq_pending();/*激活的软中断位图*/ account_system_vtime(current); /*本地禁止当前的软中断*/ __local_bh_disable((unsignedlong)__builtin_return_address(0),SOFTIRQ_OFFSET); lockdep_softirq_enter();/*current->softirq_context++*/ cpu=smp_processor_id();/*当前cpu编号*/ restart: /*Resetthependingbitmaskbeforeenablingirqs*/ set_softirq_pending(0);/*重置位图*/ local_irq_enable(); h=softirq_vec; do{ if(pending&1){ unsignedintvec_nr=h-softirq_vec;/*软中断索引*/ intprev_count=preempt_count(); kstat_incr_softirqs_this_cpu(vec_nr); trace_softirq_entry(vec_nr); h->action(h);/*调用软中断的处理函数*/ trace_softirq_exit(vec_nr); if(unlikely(prev_count!=preempt_count())){ printk(KERN_ERR"huh,enteredsoftirq%u%s%p""withpreempt_count%08x," "exitedwith%08x?\n",vec_nr,softirq_to_name[vec_nr],h->action,prev_count, preempt_count()); } rcu_bh_qs(cpu); } h++; pending>>=1; }while(pending); local_irq_disable(); pending=local_softirq_pending(); if(pending&--max_restart)/*重复触发*/ gotorestart; /*如果重复触发了10次了,接下来唤醒ksoftirqd/n内核线程来处理*/ if(pending) wakeup_softirqd(); lockdep_softirq_exit(); account_system_vtime(current); __local_bh_enable(SOFTIRQ_OFFSET); }
(4) ksoftirqd内核线程
内核不会立即处理重新触发的软中断。
当大量软中断出现的时候,内核会唤醒一组内核线程来处理。
这些线程的优先级最低(nice值为19),这能避免它们跟其它重要的任务抢夺资源。
但它们最终肯定会被执行,所以这个折中的方案能够保证在软中断很多时用户程序不会
因为得不到处理时间而处于饥饿状态,同时也保证过量的软中断最终会得到处理。
每个处理器都有一个这样的线程,名字为ksoftirqd/n,n为处理器的编号。
[java]view plain copy staticintrun_ksoftirqd(void*__bind_cpu) { set_current_state(TASK_INTERRUPTIBLE); current->flags|=PF_KSOFTIRQD;/*Iamksoftirqd*/ while(!kthread_should_stop()){ preempt_disable(); if(!local_softirq_pending()){/*如果没有要处理的软中断*/ preempt_enable_no_resched(); schedule(); preempt_disable(): } __set_current_state(TASK_RUNNING); while(local_softirq_pending()){ /*Preemptdisablestopscpugoingoffline. *Ifalreadyoffline,we'llbeonwrongCPU:don'tprocess. */ if(cpu_is_offline(long)__bind_cpu))/*被要求释放cpu*/ gotowait_to_die; do_softirq();/*软中断的统一处理函数*/ preempt_enable_no_resched(); cond_resched(); preempt_disable(); rcu_note_context_switch((long)__bind_cpu); } preempt_enable(); set_current_state(TASK_INTERRUPTIBLE); } __set_current_state(TASK_RUNNING); return0; wait_to_die: preempt_enable(); /*Waitforkthread_stop*/ set_current_state(TASK_INTERRUPTIBLE); while(!kthread_should_stop()){ schedule(); set_current_state(TASK_INTERRUPTIBLE); } __set_current_state(TASK_RUNNING); return0; }
下半部目前包括软中断,tasklet,工作队列。
软中断:
编译器静态分配的; 不互相抢占; 只有中断处理程序可以抢占它; 相同类型软中断可以在不同的CPU上同时运行; 大部分软中断处理程序都通过采取单处理器数据或其他技巧来避免加锁。
tasklet:
建立在软中断之上;
可以动态生成;
root:/etc:# ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 04:33 ? 00:00:00 init [5]
root 2 1 0 04:33 ? 00:00:00 [ksoftirqd/0] -----------------软中断,其包括分很多类型:
(enum
{
HI_SOFTIRQ=0,
TIMER_SOFTIRQ, --------- 定时器
NET_TX_SOFTIRQ,
NET_RX_SOFTIRQ, --------- 网络系统,对时间要求严格,所以直接使用软中断。
BLOCK_SOFTIRQ,
TASKLET_SOFTIRQ,
SCHED_SOFTIRQ,
#ifdef CONFIG_HIGH_RES_TIMERS
HRTIMER_SOFTIRQ,
#endif
RCU_SOFTIRQ,/* Preferable RCU should always be the last softirq */
};)
root 3 1 0 04:33 ? 00:00:00 [watchdog/0]
root 4 1 0 04:33 ? 00:00:00 [events/0] ------------- 工作队列
root 5 1 0 04:33 ? 00:00:00 [khelper]
root 6 1 0 04:33 ? 00:00:00 [kthread]
root 35 6 0 04:33 ? 00:00:00 [kblockd/0]
root 49 6 0 04:33 ? 00:00:00 [pdflush]
root 50 6 0 04:33 ? 00:00:00 [pdflush]
root 51 6 0 04:33 ? 00:00:00 [kswapd0]
root 52 6 0 04:33 ? 00:00:00 [aio/0]
root 169 1 0 04:33 ? 00:00:00 [mtdblockd]
root 192 6 0 04:33 ? 00:00:00 [kjournald]
root 216 6 0 04:33 ? 00:00:00 [kjournald]
root 219 6 0 04:33 ? 00:00:00 [kjournald]
asmlinkage void __init start_kernel(void)
{
init_IRQ();
init_timers();
hrtimers_init();
softirq_init();
rest_init(); --------- 其中创建了initd和kthreadd两个内核线程,且kthreadd处于休眠状态。
}
1、
static __init int spawn_ksoftirqd(void)
{
void *cpu = (void *)(long)smp_processor_id();
int err = cpu_callback(&cpu_nfb,CPU_UP_PREPARE, cpu);--- 此时创建并阻塞内核线程ksoftirqd/0,ksoftirqd/1等
BUG_ON(err == NOTIFY_BAD);
cpu_callback(&cpu_nfb,CPU_ONLINE, cpu);--- 此时唤醒内核线程
register_cpu_notifier(&cpu_nfb);
return 0;
}
early_initcall(spawn_ksoftirqd);---- 每个CPU都启动了一个"ksoftirqd/%d"内核线程。
static int __cpuinit cpu_callback(struct notifier_block *nfb,
unsigned long action,
void *hcpu)
{
int hotcpu = (unsigned long)hcpu;
struct task_struct *p;
switch (action) {
case CPU_UP_PREPARE:
case CPU_UP_PREPARE_FROZEN:
p =kthread_create(ksoftirqd, hcpu, "ksoftirqd/%d", hotcpu);
if (IS_ERR(p)) {
printk("ksoftirqd for %i failed\n", hotcpu);
return NOTIFY_BAD;
}
kthread_bind(p, hotcpu);
per_cpu(ksoftirqd, hotcpu) = p;
break;
case CPU_ONLINE:
case CPU_ONLINE_FROZEN:
wake_up_process(per_cpu(ksoftirqd, hotcpu));
break;
#ifdef CONFIG_HOTPLUG_CPU
case CPU_UP_CANCELED:
case CPU_UP_CANCELED_FROZEN:
if (!per_cpu(ksoftirqd, hotcpu))
break;
/* Unbind so it can run. Fall thru. */
kthread_bind(per_cpu(ksoftirqd, hotcpu),
any_online_cpu(cpu_online_map));
case CPU_DEAD:
case CPU_DEAD_FROZEN: {
struct sched_param param = { .sched_priority = MAX_RT_PRIO-1 };
p = per_cpu(ksoftirqd, hotcpu);
per_cpu(ksoftirqd, hotcpu) = NULL;
sched_setscheduler_nocheck(p, SCHED_FIFO, ¶m);
kthread_stop(p);
takeover_tasklets(hotcpu);
break;
}
#endif /* CONFIG_HOTPLUG_CPU */
}
return NOTIFY_OK;
}
2、kthreadd平时休眠,被唤醒创建对应的内核线程;
软中断softirq,工作队列workqueue等通过kthread_create_on_node接口,激活kthreadd线程,进而创建对应的内核线程。创建完后,继续休眠进程。
此内核线程的调度策略为SCHED_NORMAL,优先级为0。//不同内核版本实现不同。
3、软中断直接执行位置
local_bh_enableirq_exitnetif_rx_ni,接收到网络报文,如果不是在中断状态下,则执行软中断;否则退出;
4、软中断内核线程被创建成功后,默认为休眠状态,有很多唤醒时机:
在中断退出irq_exit时,如果不是在中断状态下,则执行软中断;否则,唤醒ksoftirqd去执行软中断;软中断过多,一次执行不完,则唤醒ksoftirqd;(分do_softirq一次执行不完,软中断内核线程一次执行不完两种情况。)tasklet_schedule,如果不是在中断状态下,则唤醒ksoftirqd;如果是中断状态,则不执行,认为退出中断时会执行软中断。 相同类型软中断可以在不同的CPU上同时运行;