前言
关于linux的软中断的文章,在网上可以找到很多,但总觉着讲的都不够深入,打算自己写一下
软中断的感性认识
中断一旦被触发,本地cpu正在运行的不管是什么程序都要让路,让中断程序执行并且执行过程中不能被打断。在linux下,符合这种情景的中断有两种:
- 硬中断
即硬件机制,GIC产生一个中断后通知cpu,cpu硬件会跳到特定的地址去执行程序,不能被打断, 这个程序就是中断服务程序。 - 软中断
用软件编程的方式来模拟硬件中断的特性,当触发软中断任务执行时,此任务是不能被本core上的其他任务抢占,运行优先级最高,软中断任务执行结束后,才能执行其他任务,类似于硬中断的行为。但是毕竟是软件模拟的行为,所以软中断任务在执行的过程中是可以接收硬中断并优先执行中断服务程序(硬中断是老大,优先处理)。
那问题来了,软中断是如何做到不被本core上的其他任务抢占呢?我们先要说一下其他方面的知识
基础知识
运行上下文
大家都知道,在linux内核里用current_thread_info来表示本core上当前所运行程序的信息。
union thread_union {
struct thread_info thread_info;
unsigned long stack[THREAD_SIZE/sizeof(long)];
};
static inline struct thread_info *current_thread_info(void) __attribute_const__;
static inline struct thread_info *current_thread_info(void)
{
register unsigned long sp asm ("sp");
return (struct thread_info *)(sp & ~(THREAD_SIZE - 1));
}
从上面的定义可以看出,thread_info与栈共同占用一个page,thread_info从低地址开始存放,而栈从高地址往低地址增长。由于页对齐的缘故,栈指针sp & ~(THREAD_SIZE - 1)就是thread_info。
其次,内核程序在运行的过程中,需要有一个变量(preempt_count)
//#define preempt_count() (current_thread_info()->preempt_count)
current_thread_info()->preempt_count
来标明目前此程序正在运行的环境(即上下文),在linux中,用不同的整数来代表不同的上下文。
//这里的 PREEMPT_SHIFT 是什么值就不做讨论了。
#define PREEMPT_OFFSET (1UL << PREEMPT_SHIFT)
#define SOFTIRQ_OFFSET (1UL << SOFTIRQ_SHIFT)
#define HARDIRQ_OFFSET (1UL << HARDIRQ_SHIFT)
#define NMI_OFFSET (1UL << NMI_SHIFT)
比如,来了个中断,当前任务被打断,那么这时候此任务应该被标识为正在运行在中断上下文中
比如来个硬中断,
//# define add_preempt_count(val) do { preempt_count() += (val); } while (0)
//# define sub_preempt_count(val) do { preempt_count() -= (val); } while (0)
add_preempt_count(HARDIRQ_OFFSET);
即把 preempt_count变量加上 HARDIRQ_OFFSET后的值,就表示当前current正运行在中断上下文。
随之而来的问题就出现了,preempt_count存在的意义是什么呢?,仅仅就是为了标识当前程序所运行的上下文吗?,我们接着说。
linux的抢占调度
上面的问题跟linux的调度器有关,我们都知道,linux内核是可抢占的,关于内核程序之间的抢占调度执行其实分为两个阶段。
- check点(检查当前线程是否需要被调度出去)
比如说 tick时钟到了后, 通过某个调度算法(CFS)判断当前任务是否应该被调度(比如运行时间到了),然后将 current_thread_info()->flags = TIF_NEED_RESCHED
通过代码感受一下流程
//sunxi_timer.c
//首先, 平台必须要注册一个定时器,用来触发tick中断用
clockevents_register_device(&sunxi_clockevent);
clockevents_register_device(&sunxi_clockevent);
clockevents_register_device
list_add(&dev->list, &clockevent_devices);
clockevents_do_notify(CLOCK_EVT_NOTIFY_ADD, dev);
ret = nb->notifier_call(nb, val, v);
tick_check_new_device
tick_setup_device
tick_setup_periodic
tick_set_periodic_handler
dev->event_handler = tick_handle_periodic;
//tick中断的ISR
sunxi_timer_interrupt
evt->event_handler(evt);
//event_handler
//tick中断来到后,执行此函数
tick_handle_periodic
tick_periodic
update_process_times
scheduler_tick
//CFS算法
curr->sched_class->task_tick(rq, curr, 0);
task_tick_fair
entity_tick
//如果当前进程需要被调度出去的话,则flags置位:TIF_NEED_RESCHED
check_preempt_tick
resched_task
set_tsk_need_resched
set_tsk_thread_flag(tsk,TIF_NEED_RESCHED);
- 发生抢占(实际发生线程切换)
实际发生线程切换,发生在中断函数要返回的时候,我们看一下
//GIC发生中断,会调到 __irq_svc(中断向量)这里执行
__irq_svc:
svc_entry
irq_handler //执行中断函数
#ifdef CONFIG_PREEMPT
get_thread_info tsk //得到当前运行程序的thread_info 结构体
//接下来就是重点,如果 thread_info 的结构体中的 preempt_count 的值不为0
//并且flags的值是 TIF_NEED_RESCHED,则执行svc_preempt
ldr r8, [tsk, #TI_PREEMPT] @ get preempt count
ldr r0, [tsk, #TI_FLAGS] @ get flags
teq r8, #0 @ if preempt count != 0
movne r0, #0 @ force flags to 0
tst r0, #_TIF_NEED_RESCHED
blne svc_preempt
#endif
#ifdef CONFIG_PREEMPT
svc_preempt:
mov r8, lr
//执行 preempt_schedule_irq 函数完成线程切换调度
1: bl preempt_schedule_irq @ irq en/disable is done inside
ldr r0, [tsk, #TI_FLAGS] @ get new tasks TI_FLAGS
tst r0, #_TIF_NEED_RESCHED
moveq pc, r8 @ go again
b 1b
#endif
由上面的分析可知, 首先,完成线程切换,必须要满足
- 当前执行的线程是需要被调度的(flags == TIF_NEED_RESCHED)
- 当前执行的线程的是可被抢占(preempt_count == 0)
至此,我们发现, preempt_count 的作用,即在实际发生调度切换时,如果处在中断上下文中(硬中断,软中断),则当前进程是不能被调度的。
软中断的软件实现
在文章的最开始,我们说过这样的话
当触发软中断任务执行时,此任务是不能被本core上的其他任务抢占,运行优先级最高,软中断任务执行结束后,才能执行其他任务
那如何实现呢? 通过上面的知识,就不难分析出了, 当要执行软中断任务时,会调用__do_softirq函数,此函数刚开始就会调用 __local_bh_disable 函数将当前进程的上下文设置为 softirq环境(软中断环境)
add_preempt_count(SOFTIRQ_OFFSET);
实际上就是:preempt_count = SOFTIRQ_OFFSET;
设置preempt_count 变量后,在执行softirq的任务(运行在本core上的线程会执行特定的函数们,每个函数即为一个软中断的任务),即软中断任务在执行的过程中是处在preempt_count 变量 非0的状态下,所以此时本core上如果在来一个tick中断,中断返回时,因为preempt_count 不为0所以当前线程(正在执行软中断的任务)不会被调度出去,实现了在本core上不能被其他任务抢占的机制。
当软中断执行之后,会 __local_bh_enable,即 preempt_count -= SOFTIRQ_OFFSET,允许被抢占
顺便提一嘴spinlock
其实spinlock也是一样的机制,spinlock大家都知道,核之间硬件锁,核内锁调度。
其中核内锁调度,即不能在本core上被调度出去,也是通过preempt_count 设置了个不能于0的值来实现的。
基于代码,理解softirq
接下来进入正题,首先看一张图,了解下大概的软件脉络