zoukankan      html  css  js  c++  java
  • Linux内核软中断

    1 软中断概述

    软中断是实现中断下半部的一种手段,与2.5以前版本的下半段机制不同。软中断可以同时运行在不同的CPU上。

    1.1 软中断的表示

    内核中用结构体softirq_action表示一个软中断。软中断是一组静态定义的接口,有32个。但是内核(2.6.34)中只实现了10个。可用的软中断的个数用NR_SOFTIRQ表示,NR_SOFTIRQ=10,软中断的类型用一个枚举体表示。这里需要注意的是,32个软中断体现在位掩码是unsigned int 类型。

    static struct softirq_action softirq_vec[NR_SOFTIRQS] ;
    enum
    {
    	HI_SOFTIRQ=0,
    	TIMER_SOFTIRQ,
    	NET_TX_SOFTIRQ,
    	NET_RX_SOFTIRQ,
    	BLOCK_SOFTIRQ,
    	BLOCK_IOPOLL_SOFTIRQ,
    	TASKLET_SOFTIRQ,
    	SCHED_SOFTIRQ,
    	HRTIMER_SOFTIRQ,
    	RCU_SOFTIRQ,	/* Preferable RCU should always be the last softirq */
    
    	NR_SOFTIRQS
    };
    struct softirq_action
    {
    	void	(*action)(struct softirq_action *);
    };
    

    2 软中断相关的数据结构

    2.1 thread_info的preempt_count字段

    preempt_count 是一个32位的int型,共分为5个字段
    这里写图片描述
    宏in_interrupt检测软中断计数器 硬中断计数器 和 NMI掩码,只要这三个字段任意一个字段不为0,就表示进程处于中断上下文。

    #define in_irq()		(hardirq_count())
    #define in_softirq()		(softirq_count())
    #define in_interrupt()		(irq_count())
    

    2.2 pending位掩码

    每个CPU上都有一个irq_stat结构,irq_stat中的__softirq_pending是一个32位的掩码,为1表示该软中断已经激活,正等待处理。为0表示软中断被禁止。在do_irq中被使用。
    内核使用local_softirq_pending得到当前CPU上的位掩码

    #define local_softirq_pending()	percpu_read(irq_stat.__softirq_pending)
    #define set_softirq_pending(x)	percpu_write(irq_stat.__softirq_pending, (x))
    #define or_softirq_pending(x)	percpu_or(irq_stat.__softirq_pending, (x))
    
    irq_cpustat_t irq_stat[NR_CPUS] 
    typedef struct {
            ...
    	unsigned int __softirq_pending;
            ...
    }irq_cpustat_t;
    

    2.3 软中断栈

    进程的内核栈的大小根据编译时选项不同,可以是4K或者8K。如果是8K堆栈,中断,异常和软中断(softirq) 共享这个堆栈。如果选择4K堆栈,则内核堆栈 硬中断堆栈 软中断堆栈各自使用一个4K空间。关于软中断堆栈,后面在软中断处理时再详细说明。

    #ifdef CONFIG_4KSTACKS
        
    static DEFINE_PER_CPU_PAGE_ALIGNED(union irq_ctx, softirq_stack);
    
    union irq_ctx {
    	struct thread_info      tinfo;
    	u32                     stack[THREAD_SIZE/sizeof(u32)];
    } __attribute__((aligned(PAGE_SIZE)));
    

    3 软中断的初始化

    内核使用open_softirq初始化一个软中断,nr是代表软中断类型的常量,action指向一个软中断处理函数

    void open_softirq(int nr, void (*action)(struct softirq_action *))
    {
    	softirq_vec[nr].action = action;
    }
    

    4 软中断的触发(raise softirq)

    触发就是将位掩码pending的相应位 置1的过程。内核使用raise_softirq完成触发软中断,nr是要触发的软中断类型。值的注意的是,中断的触发 发生关闭硬中断的情况下。

    触发软中断的过程中,如果该进程未处于中断上下文,说明当前进程处于进程上下文中,那么我们直接调用wakeup_softirqd调度ksoftirqd即可。

    反之,如果当前处于中断上下文中或软中断被禁止使用,那么就不必调度内核线程,在中断处理后期irq_exit中,会调用invoke_softirq()处理软中断。
    实际的工作是交给or_softirq_pending(1UL << (nr)); 完成的,该函数通过位操作将指定为和pending相加。

    这里写图片描述

    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);
    
    	/*
    	 * If we're in an interrupt or softirq, we're done
    	 * (this also catches softirq-disabled code). We will
    	 * actually run the softirq once we return from
    	 * the irq or softirq.
    	 *
    	 * Otherwise we wake up ksoftirqd to make sure we
    	 * schedule the softirq soon.
             * 如果不在中断上下文中 
    	 */
    	if (!in_interrupt())
    		wakeup_softirqd();
    }
    
    #define __raise_softirq_irqoff(nr) do { or_softirq_pending(1UL << (nr)); }          
    

    5. 软中断的处理

    5.1 处理软中断的时机

    1 在do_irq的末期(irq_exit)

    如果当前进程没有处于中断上下文中并且本地CPU上还有没有处理的软中断,那么就调用invoke_softirq()处理软中断。

    #ifdef __ARCH_IRQ_EXIT_IRQS_DISABLED
    # define invoke_softirq()	__do_softirq()
    #else
    # define invoke_softirq()	do_softirq()
    #endif
    void irq_exit(void)
    {    
    	account_system_vtime(current);
    	trace_hardirq_exit();
    	sub_preempt_count(IRQ_EXIT_OFFSET);
    	if (!in_interrupt() && local_softirq_pending())
    		invoke_softirq();
    
    	rcu_irq_exit();
    #ifdef CONFIG_NO_HZ
    	/* Make sure that timer wheel updates are propagated */
    	if (idle_cpu(smp_processor_id()) && !in_interrupt() && !need_resched())
    		tick_nohz_stop_sched_tick(0);
    #endif
    	preempt_enable_no_resched();
    }
    
    #ifdef __ARCH_IRQ_EXIT_IRQS_DISABLED
    # define invoke_softirq()	__do_softirq()
    #else
    # define invoke_softirq()	do_softirq()
    #endif
    
    

    程序5-1 irq_exit

    2 当软中断被重复触发超过10次时,内核会调用wakeup_softirqd()唤醒内核线程ksoftirqd去处理软中断。

    5.2 软中断的处理

    1 do_softirq

    根据内核堆栈大小,有两种do_softirq,一种是通用do_irq,另一种是架构相关的do_irq(arch/x86/kernel/irq_32.c)。

    通用的do_irq的工作流程
    1 判断当前是否处于硬中断上下文中或者软中断被禁用,如果是那么直接返回
    2 保存Eflags 然后关闭本地硬件中断
    3 获取本地CPU的位掩码pending 如果有待处理的软中断就调用__do_irq
    4 从__do_irq返回恢复Eflags

    asmlinkage void do_softirq(void)
    {
            //用来保存位掩码的局部变量
    	__u32 pending;
            //保存Eflags寄存器的局部变量
    	unsigned long flags;
            //如果do_softirq在中断上下文中被调用 或 软中断被禁止使用 那么不处理软中断
            //直接返回
    	if (in_interrupt())
    		return;
            //将Eflags保存到flags中 然后关硬件中断 
    	local_irq_save(flags);
            //获取本地CPU上的位掩码
    	pending = local_softirq_pending();
            
    	if (pending)
    		__do_softirq();
            //将flags
    	local_irq_restore(flags);
    }
    

    x86_32架构下使用4K软中断堆栈的do_softirq处理流程
    1 类似于通用的do_softirq,如果在中断上下文中或者软中断被禁止使用就立即返回。然后关外部中断
    2 如果本地CPU上存在待处理的软中断就开始对软中断堆栈的处理,关键是令isp指向软中断堆栈的栈底。然后在软中断栈上调用call_on_stack。call_on_stack是一段内联汇编,其主要目的是完成从内核栈到软中断栈的切换。先将esp保存到ebx中,使用call指令跳转到__do_softirq子例程,子例程返回时再恢复esp。

    asmlinkage void do_softirq(void)
    {
    	unsigned long flags;
    	struct thread_info *curctx;
    	union irq_ctx *irqctx;
    	u32 *isp;
    
    	if (in_interrupt())
    		return;
    
    	local_irq_save(flags);
    
    	if (local_softirq_pending()) {
                    //curctx指向当前进程的thread_info结构
    		curctx = current_thread_info();
                    //irqctx包含一个软中断堆和thread_info结构
    		irqctx = __get_cpu_var(softirq_ctx);
                    //触发硬中断和软中断是同一个进程所以将threadinfo的task指针统一
    		irqctx->tinfo.task = curctx->task;
                    //从内核堆栈切换到软中断堆栈 需要保存内核堆栈的栈指针寄存器内容
    		irqctx->tinfo.previous_esp = current_stack_pointer;
    
    		/* build the stack frame on the softirq stack */
                    //isp指向软中断栈底
    		isp = (u32 *) ((char *)irqctx + sizeof(*irqctx));
                    //在软中断堆栈上调用__do_softirq
    		call_on_stack(__do_softirq, isp);
    	}
    	local_irq_restore(flags);
    }
    
    static void call_on_stack(void *func, void *stack)
    {
            //call *%%edi 间接绝对近调用 偏移地址存储在edi寄存器中
            //指令执行时 先将eip入栈 然后将edi-->eip
            //先交换ebx and esp
            //然后调用__do_softirq
            //然后将ebx-->esp 恢复xchgl之前的esp
            //输出约束将ebx --> stack
    	asm volatile("xchgl	%%ebx,%%esp	
    "
    		     "call	*%%edi		
    "
    		     "movl	%%ebx,%%esp	
    "
    		     : "=b" (stack)
    		     : "0" (stack),
    		       "D"(func)
    		     : "memory", "cc", "edx", "ecx", "eax");
    }
    

    2 __do_softirq

    软中断的处理实际上是由 __do_softirq完成,整体的思路是遍历pending,如果某一位不为空表示本地CPU上有待处理的软中断,然出调用软中断的处理函数。
    开始处理软中断前,内核要调用__local_bh_disable(通过将preempt_count的软中断计数器加1)关闭下半部。如前所说,处理软中断的时机不止一个,内核要保证在本地CPU上软中断的处理是串行的。
    另外在处理软中断的循环结束时,内核还要检测是否有重复触发的软中断。先调用local_softirq_pending()获取位掩码pending,然后根据pending继续处理软中断,不过这种重复处理不能超过10次(MAX_SOFTIRQ_RESTART),一旦超过10次,内核就会唤醒ksoftirqd

    #define MAX_SOFTIRQ_RESTART 10
    
    asmlinkage void __do_softirq(void)
    {
            // softirq_action表示一个软中断
    	struct softirq_action *h;
            // 局部变量pending 保存待处理软中断位图
    	__u32 pending;
            // 软中断的重启次数
    	int max_restart = MAX_SOFTIRQ_RESTART;
    	int cpu;
            //获取本地CPU上所有待处理的软中断
    	pending = local_softirq_pending();
    	account_system_vtime(current);
    
            //关闭下半部中断
    	__local_bh_disable((unsigned long)__builtin_return_address(0));
    	lockdep_softirq_enter();
    
    	cpu = smp_processor_id();
    restart:
    	/* Reset the pending bitmask before enabling irqs */
            //pending已经保存了所有带出软中断的状态 所以将pending bitmask clear
    	set_softirq_pending(0);
            // 开中断
    	local_irq_enable();
            //h指向第一类软中断
    	h = softirq_vec;
    	do {
                    //先处理第一类软中断
    		if (pending & 1) {
    			int prev_count = preempt_count();
    			kstat_incr_softirqs_this_cpu(h - softirq_vec);
    
    			trace_softirq_entry(h, softirq_vec);
                            //调用软中断处理程序
    			h->action(h);
    			trace_softirq_exit(h, softirq_vec);
    			if (unlikely(prev_count != preempt_count())) {
    				printk(KERN_ERR "huh, entered softirq %td %s %p"
    				       "with preempt_count %08x,"
    				       " exited with %08x?
    ", h - softirq_vec,
    				       softirq_to_name[h - softirq_vec],
    				       h->action, prev_count, preempt_count());
    				preempt_count() = prev_count;
    			}
    
    			rcu_bh_qs(cpu);
    		}
    		h++;
    		pending >>= 1;
    	} while (pending);
    
            //关闭本地CPU上的硬中断
    	local_irq_disable();
            //获取位掩码
    	pending = local_softirq_pending();
    	if (pending && --max_restart)
    		goto restart;
    
    	if (pending)
    		wakeup_softirqd();
    
    	lockdep_softirq_exit();
    
    	account_system_vtime(current);
       //将软中断计数器加1,激活软中断
    	_local_bh_enable();
    }
    

    6 ksoftirqd内核线程

    6.1 ksoftirqd

    在内核处理类似NET_RX_SOFTIRQ的软中断时,如果有大量等待处理的数据包。就会不断的调用
    __raise_softirq_irqoff(NET_RX_SOFTIRQ)重复触发软中断NET_RX_SOFTIRQ。这样做会导致用户进程的” 饥饿问题 “ (长时间无法获得CPU)。
    针对这种问题内核使用内核线程ksoftirqd去处理自行触发次数超过10次的软中断。

    6.2 ksoftirqd的实现

    static int ksoftirqd(void * __bind_cpu)
    {
            //ksoftirqd的优先级最低(nice = 19)
    	set_user_nice(current, 19);
            //将ksoftirqd设置为不可冻结
    	current->flags |= PF_NOFREEZE;
            //设置ksoftirqd为可中断状态
        	set_current_state(TASK_INTERRUPTIBLE);
           
    	while (!kthread_should_stop()) {
                    //如果没有待处理的软中断则 调度别的进程
    		if (!local_softirq_pending())
    			schedule();
    
    		__set_current_state(TASK_RUNNING);
    
    		while (local_softirq_pending()) {
    			/* Preempt disable stops cpu going offline.
    			   If already offline, we'll be on wrong CPU:
    			   don't process */
                            //关闭内核抢占
    			preempt_disable();
                            //处理软中断
    			do_softirq();
                            //开启内核抢占
    			preempt_enable();
                            //cond_resched()的目的是提高系统实时性, 主动放弃cpu供优先级更高的任务使用
    			cond_resched();
    		}
                    
    		set_current_state(TASK_INTERRUPTIBLE);
    	}
    	__set_current_state(TASK_RUNNING);
    	return 0;
    }
    

    程序6-1 ksoftirqd主功能函数

    out:
    	local_irq_enable();
    	return;
    
    softnet_break:
    	__get_cpu_var(netdev_rx_stat).time_squeeze++;
    	__raise_softirq_irqoff(NET_RX_SOFTIRQ);
    	goto out;
    

    程序6-2 net_rx_action

    void wakeup_softirqd(void)
    {
    	/* Interrupts are disabled: no need to stop preemption */
    	struct task_struct *tsk = __get_cpu_var(ksoftirqd);
    
    	if (tsk && tsk->state != TASK_RUNNING)
    		wake_up_process(tsk);
    }
    

    程序6-3 wakeup_softirqd

    还未解决的问题

    set_current_state和 __set_current_state的区别
    PF_NOFREEZE
    cond_resched()
    为什么在do_softirq开始处理软中断时要关闭硬件中断

    参考

    ULK
    http://blog.csdn.net/hardy_2009/article/details/7383729 关于内核栈和中断栈的说明

  • 相关阅读:
    Powershell分支条件
    Powershell基础
    初识PowerShell
    设计模式--策略模式
    设计模式--简单工程模式
    StandardWrapper
    Tomcat的安全性
    算法效率 简单的增长率 参照

    排序算法之 归并排序
  • 原文地址:https://www.cnblogs.com/dennis-wong/p/14729418.html
Copyright © 2011-2022 走看看