中断和异常
同步中断(异常):软件产生,只有执行了某条指令之后才会发生
异步中断(中断):硬件产生,随时可能发生
一、 中断信号的作用
中断是一个内核控制路径,它代表中断发生时的进程在内核中执行。
Ø 中断随时可能发生,因此内核处理中断要尽量的快,把更多的处理向后推迟:关键而紧急的部分内核立即执行;其余部分内核推迟到后续执行
Ø 中断程序必须可以嵌套执行
Ø 内核的临界区必须关中断执行
二、 中断和异常
Ø 中断
² 可屏蔽中断:一般的I/O设备中断
² 不可屏蔽中断:只有危急时间才是不可屏蔽中,如硬件故障
Ø 异常
² 故障:可以纠正,异常处理结束之后,引发故障的那条指令会重新执行
² 陷阱:
² 异常中止:
² 编程异常:
三、 中断和异常处理程序的嵌套执行
每个中断或者异常都会引发一个内核控制路径,代表当前进程在内核态执行单独的指令序列。
内核允许控制路劲嵌套执行,其代价是:中断处理程序永远不能被阻塞,即中断处理程序运行期间不能发生进程切换。因为中断处理程序是代表当前进程在内核执行特定的指令序列,因此,中断的一些数据是保存在当前进程的内核态栈中的,如果发生进程切换,后续要恢复中断继续执行就要等到被挂起的进程重新获得cpu,但是被挂起进程何时会获得CPU是不可而知的,这样中断可能会等待很久才会得到执行,这对中断处理来说是不可容忍的。
如果内核没有bug,则大多数的异常是在用户态发生的,缺页异常发生在内核态,在处理这个异常的时候,进程运行被挂起,用另外一个进程代替它,直到请求的页可用为止,只要被挂起的进程重新获得cpu,处理缺页异常的控制路径就恢复执行。
缺页异常不会进一步引发异常。
中断可以抢占另一个中断,也可以抢占异常;但是异常从不抢占中断,中断从不执行导致缺页的操作(缺页异常这就意味这进程切换)。
Linux交错执行内核控制路径:
1.提高中断控制器和设备控制器的吞吐量。PIC接受到一个设备控制器的中断信号之后,使二者保持阻塞,直到从CPU接受到一个应答,由于LINUX的中断是交错执行的,即使在执行其他中断的时候,CPU收到一个中断信号也会立即向PIC发生应答信号。
2.实现没有优先级的中断模型。因为每个中断都可以被另外一个中断抢占,因此就没有必要给每个设备预定义中断优先级了。
四、 异常处理
异常处理的标注过程:
Ø 在内核栈上保存大多数寄存器的值
Ø 用高级C程序处理异常
Ø 使用ret_from_exception()退出异常
五、 中断处理
Ø I/O中断处理
² IRQ共享
² IRQ动态分配
Linux把中断要执行的操作分三类:
² 紧急:如对中断控制器做出应答,这样的操作需要中断程序在第一时间完成,且是在关中断的前提下执行。
² 非紧急:如按下一个键后读扫描码,这个操作也需要中断处理程序立即完成,但是在开中断的情况下执行
² 非紧急可延迟:如把把缓存区的数据读入进程的地址空间,这些操作可能耗时很久,这个操作由独立的函数完成。
中断处理程序的四个步骤:
² 在内核栈中保存IRQ的值以及寄存器的内容
² 给中断控制器发送应答
² 执行共享该IRQ的所有设备的中断服务程序ISR
² 跳转到ret_from_intr()
Ø IRQ数据结构
² IRQ描述符irq_desc
struct irq_desc {
irq_flow_handler_t handle_irq;
/*服务于中断控制器某个引脚的函数指针*/
......
void *handler_data;
/*中断控制器方法的数据指针*/
......
struct irqaction *action; /*IRQn的服务例程,组成一个单项链表*/
unsigned int status; /*IRQn的一组状态*/
unsigned int depth; /*IRQn被激活时值为0,禁止时为一个正数*/
unsigned int irq_count;/*IRQn发生了多少次中断*/
unsigned int irqs_unhandled;
spinlock_t lock;
const char *name;
}中断IRQ描述符
这些描述符组成一个数组struct irq_desc irq_desc[NR_IRQS];irq_desc[]数组。
² Irqaction描述符
struct irqaction {
irq_handler_t handler;/*指向一个I/O设备的中断服务例程*/
unsigned long flags;
/*描述IRQ与I/O设备的关系,SA_SHIRQ /SA_INTERRUPT /SA_SAMPLE_RANDOM,分别表示允许其他设备共享IRQ线、必须关中断执行、随机事件发生源*/
cpumask_t mask;
const char *name;/* I/O设备名*/
void *dev_id;
struct irqaction *next; /*链表下一个元素地址*/
int irq; /*irq线*/
struct proc_dir_entry *dir;
};
Ø 中断处理
在汇编代码的irq_handler宏中会调用asm_do_IRQ(),asm_do_IRQ()函数是中断处理程序C的代码的总入口。
asmlinkage void __exception asm_do_IRQ(unsigned int irq, struct pt_regs *regs)
{
struct pt_regs *old_regs = set_irq_regs(regs);
struct irq_desc *desc = irq_desc + irq;//按照中断号得到中断描述符
if (irq >= NR_IRQS)
desc = &bad_irq_desc;
irq_enter();//增加中断处理程序嵌套计数器
desc_handle_irq(irq, desc);//最终调用__do_IRQ()函数,后边分析
irq_exit();//中断嵌套计数器递减,检查是否有可延迟函数在等待执行
set_irq_regs(old_regs);
}
fastcall unsigned int __do_IRQ(unsigned int irq)
{
struct irq_desc *desc = irq_desc + irq;
struct irqaction *action;
unsigned int status;
spin_lock(&desc->lock);
if (desc->chip->ack)
desc->chip->ack(irq);//给中断控制器发送应答
action = NULL;
if (likely(!(status & (IRQ_DISABLED | IRQ_INPROGRESS)))) {
action = desc->action;
status &= ~IRQ_PENDING; /*发完应答了*/
status |= IRQ_INPROGRESS; /* 我正打算来处理这个中断*/
}
desc->status = status;
if (unlikely(!action))
goto out;
for (;;) {
irqreturn_t action_ret;
spin_unlock(&desc->lock);
action_ret = handle_IRQ_event(irq, action);
//循环处理这个中断号链表中注册的中断处理程序
|-----------------------------------------------------------------------------------------------------|
if (!(action->flags & IRQF_DISABLED))
local_irq_enable_in_hardirq();//开本地中断
do {
ret = action->handler(irq, action->dev_id);
if (ret == IRQ_HANDLED)
status |= action->flags;
retval |= ret;
action = action->next;//指向链表的下一个action元素
} while (action);
local_irq_disable();//关本地中断
|-----------------------------------------------------------------------------------------------------|
spin_lock(&desc->lock);
if (likely(!(desc->status & IRQ_PENDING)))
break;
desc->status &= ~IRQ_PENDING;
}
desc->status &= ~IRQ_INPROGRESS;/*中断处理结束*/
out:
desc->chip->end(irq);/*和中断控制器相关*/
spin_unlock(&desc->lock);
return 1;
}
挽救丢失的中断:enable_irq()函数强迫硬件将丢失的中断再产生一次。
Ø IRQ线动态分配
² request_irq
² setup_irq
² free_irq
六、 软中断、tasklet
内核将可以推迟执行的中断从中断处理程序中抽取抽出来,保持较短的中断响应时间。内核用两种方式实现延迟执行的中断处理:可延迟函数(软中断和tasklet)、通过工作队列执行的函数。
Softirq上实现tasklet,中断上下文:当前程序正在执行一个中断处理程序或可延迟函数。
软中断分配是静态的,tasklet的分配是动态的;即使类型相同的软中断可以在多个CPU上并发执行,不同类型的tasklet可以在多个CPU上并发执行,但是相同类型的tasklet不能并发执行,只能串行执行;所以softirq的函数必须是可重入的,tasklet不必可重入。
初始化、激活(下一轮调度中执行)、屏蔽(即使激活了也不执行)、执行,这四种可延迟函数操作。
Ø 软中断
软中断 | 下标(优先级) | 说明 |
HI_SOFTIRQ | 0 | 处理高优先级的tasklet |
TIMER_SOFTIRQ | 1 | 时钟中断相关的tasklet |
NET_TX_SOFTIRQ | 2 | 数据包传送到网卡 |
NET_RX_SOFTIRQ | 3 | 从网卡读取数据包 |
SCSI_SOFTIRQ | 4 | SCSI命令的后台中断处理 |
TASKLET_SOFTIRQ | 5 | 处理常规tasklet |
² 软中断的数据结构
static struct softirq_action softirq_vec[32];
struct softirq_action
{
void (*action)(struct softirq_action *);//指向软中断的函数指针
void *data;//软中断需要的数据
};
一个包含32个softirq_action元素的softirq_vec[32]数组,数组只有前6个元素被使用,数组的下标对应软中断的优先级。
进程描述符的thread_info字段中的preempt_count字段编码表示三个不同计数器。
preempt_count字段含义 | |
位 | 说明 |
Bit0-7 | 抢占计数器(禁用内核抢占的次数,0表示运行抢占) |
Bit8-15 | 软中断计算器(可延迟函数被禁用的程度,0表示激活) |
Bit16-27 | 硬中断计数器(本地CPU中断嵌套层数,irq_enter增加,irq_exit减少) |
Bit28 | PREEMPT_ACTIVE标志 |
² 处理软中断
open_softirq()处理软中断的初始化,raise_softirq()激活软中断,激活软中断的实际操作就是唤醒ksoftirqd内核进程,内核进程来执行所要延迟的操作。
void open_softirq(int nr, void (*action)(struct softirq_action*), void *data)
{
softirq_vec[nr].data = data;
softirq_vec[nr].action = action;
}
open_softirq()主要就是用所带的软中断号以及软中断函数指针以及函数所需的数据来初始化对应的softirq_vecp[]数组元素。
void fastcall raise_softirq(unsigned int nr)
{
unsigned long flags;
local_irq_save(flags);
raise_softirq_irqoff(nr);
/*最终调用wake_up_process(ksoftirqd) 来唤醒ksoftirqd内核进程*/
local_irq_restore(flags);
}
内核需要周期性的检查挂起/活动的软中断,比如do_IRQ()完成中断处理之后或者调用irq_exit()。
² do_softirq
asmlinkage void do_softirq(void)
{
__u32 pending;
unsigned long flags;
if (in_interrupt())
//检查preempt_count字段的0-15bit,为正数说明已经禁用软中断
return;
local_irq_save(flags);//保存IF标志,并禁中断
pending = local_softirq_pending();
if (pending)
__do_softirq();
local_irq_restore(flags);//恢复IF标志
}
² __do_softirq
执行一个软中断的时候可能出现新挂起的软中断,为保证软中断的低延迟性,__do_softirq一直循环运行直到执行完所有的软中断,但是这样可能使得__do_softirq执行较长的时间,用户态程序得不到运行,因此__do_softirq只执行一定次数的循环后就返回,其余挂起的软中断会在不久之后的ksoftirqd内核线程中得到执行。
asmlinkage void __do_softirq(void)
{
struct softirq_action *h;
__u32 pending;
int max_restart = MAX_SOFTIRQ_RESTART;/*循环次数设置为10*/
int cpu;
pending = local_softirq_pending();/*获得本地软中断位掩码*/
__local_bh_disable((unsigned long)__builtin_return_address(0));
/*增加preempt_count字段中软中断计数器的值,即禁用软中断,因为软中断大多在开中的情况下执行,在执行期间可能会产生新的中断,在do_IRQ执行irq_exit的时候可能另一个__do_softirq函数的实例开始执行,可延迟函数应该以串行的方式执行,所以要避免并发的发生,在这里禁用软中断,可使得新的函数实例在运行的第一步就退出*/
cpu = smp_processor_id();
restart:
/* Reset the pending bitmask before enabling irqs */
set_softirq_pending(0);
/*清除本地CPU软中断的位图,以便可以接受新的软中断*/
local_irq_enable();/*开中断*/
h = softirq_vec;/*把软中断数组地址保存在局部变量h中*/
do {
if (pending & 1) {
/*从pending的最低位开始循环检查位图,即由高优先级到低优先级*/
h->action(h);/*如果该为存在被挂起的软中断则执行它的函数*/
rcu_bh_qsctr_inc(cpu);
}
h++;/*h指向数组的下一个元素*/
pending >>= 1;
/*把位图的bit1移动到bit0,以便在下次循环的开始处if (pending & 1) 做判断*/
} while (pending);
local_irq_disable();
/*关本地中断,因为后边要访问CPU软中断掩码,如果中断开启的情况下,新来的中断可能会对位掩码作写操作,这样就会产生访问冲突*/
pending = local_softirq_pending();
/*读取CPU软中断的位掩码,因为在前面开中断执行的期间可能会产生新的中断,中断处理程序又会挂起新的软中断,在结束一次扫描之后再次检查是否有挂起的软中断需要执行,这样软中断可以得到快速的执行*/
if (pending && --max_restart)
goto restart;
/*如果位掩码为0,或者达到执行循环的次数就不再继续执行软中断,因为这样会耗时较多,阻碍了用户态的程序的执行*/
if (pending)
wakeup_softirqd();
/*如果达到了执行循环的最大次数,而不得不放弃继续执行软中断的话,还有最后一招,那就是唤醒ksoftirqd内核进程,让内核进程替代我们来执行未及时处理的软中断,在不就之后,ksoftirqd得到了CPU就会帮助我们来完成未做完的事情*/
_local_bh_enable();/*软中断计数器减一,重新几乎可延迟函数*/
}
² ksoftirqd内核线程
for(;;)
{
set_current_state(TASK_INTERRUPTIBLE);
schedule();
while(local_softirq_pending())
{
preempt_disable();
do_softirq();
preempt_enable();
cond_resched();
}
}
获取软中断掩码,调用do_softirq执行软中断处理,如果没有需要处理的软中断则把进程状态改为TASK_INTERRUPTIBLE,随后调用cond_resched()实现进程切换。
Ksoftirqd内核线程提供了一种平衡的解决方法: do_softirq确定哪些软中断是挂起的并执行它们,执行期间中断是开着的,因此执行期间会由中断挂起新的软中断,但是do_softirq执行最多10次的检查新的挂起软中断,超过10次还有软中断的话就唤醒内核进程ksoftirqd。内核进程有着较低的优先级,这样用户程序就有机会得到调度,如果机器空闲,软中断就会得到很快的执行。
² tasklet
驱动程序中实现可延迟函数的首选方法。它是建立在HI_SOFTIRQ、 TASKLET_SOFTIRQ的基础之上的。几个tasklet可以与一个软中断相联。
struct tasklet_struct
{
struct tasklet_struct *next;/*链表下一个元素*/
unsigned long state;
/*tasklet的状态,TASKLET_STATE_SCHED、TASKLET_STATE_RUN,前者表示tasklet被挂起在某个链表中;后者表示正在执行*/
atomic_t count;/*锁计数器*/
void (*func)(unsigned long);/*tasklet的函数指针*/
unsigned long data;/*tasklet函数所使用,可以当做指针等*/
};tasklet描述符
struct tasklet_head
{
struct tasklet_struct *list;
};
struct tasklet_head tasklet_vec[NR_CPUS]
struct tasklet_head tasklet_hi_vec[NR_CPUS]
每个cpu都在tasklet_vec[]数组和tasklet_hi_vec[]数组中各有自己的一个一个元素,这两个数组中的元素就是某个CPU上tasklet的链表头。
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;
}
tasklet_init()用于初始化一个tasklet_struct结构。tasklet_disable_nosync(),tasklet_disable()用于禁止tasklet(增加count字段),前者立即返回,后者要等到tasklet的实例运行结束才返回。tasklet_enable()激活tasklet。
static inline void tasklet_schedule(struct tasklet_struct *t)
{
unsigned long flags;
if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state))
{
local_irq_save(flags);/*保存IF标志*/
t->next = __get_cpu_var(tasklet_vec).list;
__get_cpu_var(tasklet_vec).list = t;
/*将t插入到链表首位置*/
raise_softirq_irqoff(TASKLET_SOFTIRQ);
/*唤醒TASKLET_SOFTIRQ 软中断*/
local_irq_restore(flags);/*恢复IF标志*/
}
}
tasklet_schedule(),tasklet_hi_schedule()就是将tasklet_struct结构体插入到相应的链表中,并唤醒对应的软中断TASKLET_SOFTIRQ或者HI_SOFTIRQ。
void __init softirq_init(void)
{
open_softirq(TASKLET_SOFTIRQ, tasklet_action, NULL);
open_softirq(HI_SOFTIRQ, tasklet_hi_action, NULL);
}
该函数把tasklet_action,tasklet_hi_action分别赋值给TASKLET_SOFTIRQ、HI_SOFTIRQ软中断的action字段,因此这两个软中断在执行的时候就会执行它们所挂载的tasklet,下面具体分析一下如何指向tasklet。
static void tasklet_action(struct softirq_action *a)
{
struct tasklet_struct *list;
local_irq_disable();
list = __get_cpu_var(tasklet_vec).list;/*获得链表头存放在局部变量list中*/
__get_cpu_var(tasklet_vec).list = NULL;
/*链表头清零,这样在tasklet执行期间新来的中断可以重新构建链表*/
local_irq_enable();/*开中断*/
while (list) {/*对tasklet链表做遍历*/
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 = __get_cpu_var(tasklet_vec).list;
__get_cpu_var(tasklet_vec).list = t;
__raise_softirq_irqoff(TASKLET_SOFTIRQ);
local_irq_enable();
}
}
七、 工作队列
可延迟函数执行在中断上下文,工作队列工作在进程上下文(工作者线程worker thread);中断上下文不能发生进程切换,所以可延迟函数不能执行阻塞操作,工作队列的函数可以执行导致阻塞的操作;二者皆不可以访问用户空间。
Ø 工作队列的数据结构
workqueue_struct, cpu_ workqueue_struct, work_struct三个数据结构的关系图。
Ø 工作队列函数
create_workqueue(“foo”)创建工作队列,返回新创建的工作队列的描述符workqueue_struct的地址。并创建N个工作者内核线程,N是CPU的个数。destroy_workqueue()销毁工作队列。
int fastcall queue_work(struct workqueue_struct *wq, struct work_struct *work)
{
int ret = 0;
if (!test_and_set_bit(WORK_STRUCT_PENDING, work_data_bits(work))) {
BUG_ON(!list_empty(&work->entry));
__queue_work(wq_per_cpu(wq, get_cpu()), work);
put_cpu();
ret = 1;
}
return ret;
}
/*将work_struct加入到链表,首先检查该work是不是已经在链表中了,如果不在就插入到链表,如果工作者进程睡眠则唤醒它*/
工作者进程的在worker_thread()函数内执行循环操作,进程一旦醒来就调用run_workqueue(),删除工作链表中删除所有的work_struct描述符并执行相应的挂起函数(work_struct内的func函数指针)。
Ø 预定义工作队列
内核预定义了一个叫做events的预定义工作队列,内核开发者可以随意使用,它的workqueue_struct描述符存放在keventd_wq数组中。