【linux 内核】中断下半部

中断下半部包含:软中断、tasklet、任务队列。

参考这篇文章很好:https://www.csdn.net/gather_2a/MtjaUgxsNTAzNi1ibG9n.html

    早先的中断的下半部需要(1)在任意一时刻,系统只能有一个CPU可以执行Bottom Half代码,以防止两个或多个CPU同时来执行Bottom Half函数而相互干扰。因此BH代码的执行是严格“串行化”的。(2)BH函数不允许嵌套。这使得CPU被串行化,这对SMP系统是资源的浪费,在内核升级后引入了软中断,整个softirq机制的设计与实现中自始自终都贯彻了一个思想:“谁触发,谁执行”(Who marks,Who runs),也即触发软中断的那个CPU负责执行它所触发的软中断,而且每个CPU都由它自己的软中断触发与控制机制。这个设计思想也使得softirq 机制充分利用了SMP系统的性能和特点。

    这样就可以各个CPU核心可以相互独立的处理软中断,不同的核心上可以处理同一个类型的软中断,但是在同一个核心上仍然不能被软中断打断,软中断处理流程也只能被硬中断打断,即使硬中断在返回时除了任何一个软中断, 在软中断执行流程中都不会处理这个后被触发的软中断。

    函 数do_softirq()负责执行数组softirq_vec[32]中设置的软中断服务函数。每个CPU都是通过执行这个函数来执行软中断服务的。由 于同一个CPU上的软中断服务例程不允许嵌套,因此,do_softirq()函数一开始就检查当前CPU是否已经正出在中断服务中,如果是则 do_softirq()函数立即返回。举个例子,假设CPU0正在执行do_softirq()函数,执行过程产生了一个高优先级的硬件中断,于是 CPU0转去执行这个高优先级中断所对应的中断服务程序。总所周知,所有的中断服务程序最后都要跳转到do_IRQ()函数并由它来依次执行中断服务队列 中的ISR,这里我们假定这个高优先级中断的ISR请求触发了一次软中断,于是do_IRQ()函数在退出之前看到有软中断请求,从而调用 do_softirq()函数来服务软中断请求。因此,CPU0再次进入do_softirq()函数(也即do_softirq()函数在CPU0上被 重入了)。但是在这一次进入do_softirq()函数时,它马上发现CPU0此前已经处在中断服务状态中了,因此这一次do_softirq()函数 立即返回。于是,CPU0回到该开始时的do_softirq()函数继续执行,并为高优先级中断的ISR所触发的软中断请求补上一次服务。从这里可以看 出,do_softirq()函数在同一个CPU上的执行是串行的。此处是do_softirq()并不是__do_softirq()函数。

    所以软中断是可以被硬件中断打断的,打断之后是否还会继续执行该硬中断所调用的软中断那就不一定了,要看是不是同一种类型的软中断。如下文函数中的解释,执行软中断的过程中会加禁止硬中断后读取pending,然后会将pending置0,这样在硬中断强行打断软中断的情况下,如果这个pending为0则不执行软中断处理函数。那么不同类型的软中断呢?应该是可以执行不同类型的软中断的。从__do_softirq中可以看出,硬中断的禁用和开启只加载了pending置0上,在遍历软中断执行函数的时候,h->action,每个软中断的处理函数结束时都会调用__or_softirq_pending宏再将本类型的软中断类型重新设备到本CPU的掩码上,这样一来又得出一个结论,就是高优先级的软中断仍然可以在低优先级软中断类型之前执行,这个真的厉害。比如:

(1)已知TIMER_SOFTIRQ要比NET_RX_SOFTIRQ优先级要高。

(2)__do_softirq已经执行完TIMER_SOFTIRQ的处理函数,并且TIMER_SOFTIRQ重新注册在pending上,此时开始执行NET_RX_SOFTIRQ的处理函数,这是来了一个时钟硬中断(具体怎么触发这个硬中断不了解),则当前这个软中断被打断。

(3)do__softirq这个函数在检查pending时,发现pending不为0,因为TIMER_SOFTIRQ又被重新注册上,此时认证会进入到__do_softirq函数执行软中断,在此时__do_softirq获得的pending上只会有TIMER_SOFTIRQ类型的软中断,那么就会执行这个软中断处理函数,执行完成后将控制权返回给之前的软中断上继续执行NET_RX_SOFTIRQ。注意此时并不过再次执行之前NET_RX_SOFTIRQ的软中断,因为此时的pending上只有TIMER_SOFTIRQ。

(4)从时间上来看后来的TIMER_SOFTIRQ仍然优先于NET_RX_SOFTIRQ执行完成。

    

    至于软中断是执行在进程上下文还是中断上下文我是这么理解的:对于从硬中断中最后调用遍历的软中断流程是在中断上下文执行的,引文在硬中断处理函数栈最后会调用到__do_softirq函数。对于ksoftirq进程中执行的软中断流程是在进程上下文执行的。

主流程函数:

do_softirq->__do_softirq()

【linux 内核】中断下半部_第1张图片

asmlinkage __visible void __do_softirq(void)
{
	unsigned long end = jiffies + MAX_SOFTIRQ_TIME;
	unsigned long old_flags = current->flags;
	int max_restart = MAX_SOFTIRQ_RESTART;
	struct softirq_action *h;
	bool in_hardirq;
	__u32 pending;
	int softirq_bit;

	/*
	 * 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;

	pending = local_softirq_pending();//获取当前所需要处理的软中断类型pending
	account_irq_enter_time(current);

	__local_bh_disable_ip(_RET_IP_, SOFTIRQ_OFFSET);//关闭软中断
	in_hardirq = lockdep_softirq_start();

restart:
	/* Reset the pending bitmask before enabling irqs */
	set_softirq_pending(0);//把当前cpu的软中断pending置0,这样下次硬中断或者ksoftirq进程
在调用上do_softirq函数会检查当前的pending,为0则不会再执行__do_softirq函数。
实现了单独一个cpu核心执行同一个类型执行一个软中断。

	local_irq_enable();//使能硬中断

	h = softirq_vec;

	while ((softirq_bit = ffs(pending))) {//遍历所有pending中的软中断
		unsigned int vec_nr;
		int prev_count;

		h += softirq_bit - 1;

		vec_nr = h - softirq_vec;
		prev_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())) {
			pr_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);
		}
		h++;
		pending >>= softirq_bit;
	}

	rcu_bh_qs();
	local_irq_disable();

	pending = local_softirq_pending();
	if (pending) {
		if (time_before(jiffies, end) && !need_resched() &&
		    --max_restart)
			goto restart;

		wakeup_softirqd();
	}

	lockdep_softirq_end(in_hardirq);
	account_irq_exit_time(current);
	__local_bh_enable(SOFTIRQ_OFFSET);
	WARN_ON_ONCE(in_interrupt());
	tsk_restore_flags(current, old_flags, PF_MEMALLOC);
}

pending为注册的软中断类型,以定时器软中断类型为例看看。

先看获取pending的宏:

设置pending的宏:

下面查看定时器的软中断:

【linux 内核】中断下半部_第2张图片

【linux 内核】中断下半部_第3张图片

【linux 内核】中断下半部_第4张图片

【linux 内核】中断下半部_第5张图片

【linux 内核】中断下半部_第6张图片

定时器的软中断被注册上。

 

一、软中断:

    软中断是一组静态定义的下半部接口,有32个,可以在所有处理器同时执行---即使两个类型相同的也可以。

kernel/softirq.c中定义了一个包含有32个该结构体的数组,

static struct softirq_action softirq_vec[NR_SOFTIRQS],NR_SOFTIRQS = 32.

每个被注册的软中断都占据该数组的一项,因此最多可注册32个软中断。

    执行软中断:

    一个软中断必须被标记后才会执行,这被称为触发软中断。通常,中断处理程序会在返回前标记他的软中断,使其在稍后被执行。在下列地方,待处理的软中断会被检查执行:

  1. 从一个硬件中断返回时
  2. 在ksoftirqd内核线程中
  3. 在那些显式检查和执行待处理的软中断代码中,如网络子系统中

不管用什么方法最后都是要调用do_softirq函数,这个函数会循环遍历每一个,调用他们的处理程序。

比如,do_IRQ 函数执行完硬件 ISR 后退出时调用此函数。
//
void irq_exit(void)
{
        account_system_vtime(current);
        trace_hardirq_exit();
        sub_preempt_count(IRQ_EXIT_OFFSET);
        //
        // 判断当前是否有硬件中断嵌套,并且是否有软中断在
        // pending 状态,注意:这里只有两个条件同时满足
        // 时,才有可能调用 do_softirq() 进入软中断。也就是
        // 说确认当前所有硬件中断处理完成,且有硬件中断安装了
        // 软中断处理时理时才会进入。
        // 
        if (!in_interrupt() && local_softirq_pending())
                //
                // 其实这里就是调用 do_softirq() 执行
                //
                invoke_softirq();
        preempt_enable_no_resched();
}

使用软中断:

软中断保留给系统中对时间要求最严格以及最重要的下半部使用。目前,只有两个子系统(网络和scsi)直接只用软中断。此外内核定时器和tasklet都是建立在软中断之上。

在编译期间,通过在linux/ineterrupt.h中定义的一个枚举类型来静态的声明软中断。内核用这些从0开始的索引来表示一种相对优先级。索引号小的软中断在索引号打的软中断之前执行。

【linux 内核】中断下半部_第7张图片

在运行时通过调用open_softirq()注册软中断处理函数,如网络子系统中注册自己的软中断:

open_softirq(NET_RX_SOFTIRQ, net_rx_action);

我们平台的netif_receive_skb就有net_rx_action这个软中断处理函数来调用。

软中断处理程序执行的时候,允许相应硬件中断,但是他自己不能休眠。在一个处理程序运行的时候,当前处理器上的软中断被禁止。但其他处理器上扔可以执行别的软中断,包括同种类型的软中断,这样就涉及到共享数据和加锁处理。这点很重要,它也是tasklet更受青睐的原因。单纯的禁止你的软中断处理。

引入软中断的主要原因是其可扩展性,如果不需要扩展到多处理器,那么就使用tasklet吧,tasklet本身也是软中断,只不过同一个处理器的多个实例不能再多个处理器上同时运行。

触发软中断:

标记一个软中断的函数raise_softirq(NET_RX_SOFTIRQ),追触发NET_RX_SOFTIRQ软中断,他的处理程序net_rx_action()就会在内核下一次执行软中断时投入运行。

在中断处理程序中触发软中断是最常见的形式。在这种情况下,中断处理程序执行硬件设备的相关操作,然后触发相应的软中断,最后退出。内核在执行完中断处理程序以后,马上就会调用do_softirq函数,于是软中断开始执行下半部的工作。

 

二、tasklet

 

tasklet是利用软中断实现的一种下半部机制,稍后解释tasklet和软中断有什么不同。

tasklet有两类软中断代表,HI_SOFTIRQ和TASKLET_SOFTIRQ,这两个的唯一区别就是软中断执行的先后顺序。

1、tasklet结构体

struct tasklet_struct
{
      struct tasklet_struct *next;//将多个tasklet链接成单向循环链表
      unsigned long state;//TASKLET_STATE_SCHED(Tasklet is scheduled for execution)  TASKLET_STATE_RUN(Tasklet is running (SMP only))
      atomic_t count;//0:激活tasklet 非0:禁用tasklet
      void (*func)(unsigned long); //用户自定义函数
      unsigned long data;  //函数入参
};

state 只能在0、TASKLET_STATE_SCHED和TASKLET_STATE_RUN之间取值。

2、调度tasklet

已调度的tasklet和标记触发软中断是一个意思,对应的state状态为TASKLET_STATE_SCHED,已调度的软中断会放在两个单处理器数据结构tesklet_vec(普通takslet)和tasklet_hi_vec(高优先级的tasklet)。这两个数据结构都是由tasklet_struct结构体构成的链表,链表中每个tasklet_struct代表一个不同的tasklet,每个cpu都存在这样两个结构,可以查看这两个变量的定义(https://blog.csdn.net/u011006622/article/details/89499707和https://blog.csdn.net/batoom/article/details/47808019)。

    tasklet由tasklet_schedule()和tasklet_hi_schedule()函数进行调度。

    tasklet_schedule()执行步骤:

void __tasklet_schedule(struct tasklet_struct *t)
{
	unsigned long flags;

	local_irq_save(flags);
	t->next = NULL;
	*__this_cpu_read(tasklet_vec.tail) = t;
	__this_cpu_write(tasklet_vec.tail, &(t->next));
	raise_softirq_irqoff(TASKLET_SOFTIRQ);
	local_irq_restore(flags);
}

我们事先定义了一个tasklet,一般都是申请一块内存或者是一个全局变量,调用tasklet_init()初始化这个tasklet,在某个cpu上调用tasklet_schedule(),会把这个变量的地址插入到当前cpu的tasklet_vec链表头上。

  1. 检查状态是否为TASKLET_STATE_SCHED,如果是,说明tasklet已经被调度,函数返回。如果不是,则设置上TASKLET_STATE_SCHED。
  2. 调用local_ira_save保存IF标志的状态并禁用本地中断。
  3. 在tasklet_vec[n]或者tasklet_hi_vec[n]的链表头上添加上该tasklet(n表示本地cpu的逻辑号).
  4. 调用raise_softirq_irqoff()激活TASKLET_SOFTIRQ或HI_SOFTIRQ类型的软中断。(这个函数与raise_softirq()函数类似,只是raise_softirq_irqoff()函数假设已经禁用了本地中断)
  5. 调用local_irq_restore恢复IF标志的状态。

由于大部分tasklet和软中断都是在中断处理程序中被调度也就是设置成待处理状态,所以最近一个中断返回的时候看起来就是执行do_softirq的最佳时机,它会执行相应的软中断处理程序,而这两个处理程序tasklet_action和tasklet_hi_action()就是tasklet处理的核心。

他们都做了什么:

static void tasklet_action(struct softirq_action *a)
{
	struct tasklet_struct *list;

	local_irq_disable();
	list = __this_cpu_read(tasklet_vec.head);
	__this_cpu_write(tasklet_vec.head, NULL);
	__this_cpu_write(tasklet_vec.tail, this_cpu_ptr(&tasklet_vec.head));
	local_irq_enable();

	while (list) 
    {
		struct tasklet_struct *t = list;

		list = list->next;

		if (tasklet_trylock(t)) 
        {//不是加锁,而是将tasklet设置为run
			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 = NULL;
		*__this_cpu_read(tasklet_vec.tail) = t;
		__this_cpu_write(tasklet_vec.tail, &(t->next));
		__raise_softirq_irqoff(TASKLET_SOFTIRQ);
		local_irq_enable();
	}
}

static inline int tasklet_trylock(struct tasklet_struct *t)
{
	return !test_and_set_bit(TASKLET_STATE_RUN, &(t)->state);
}
//这里设置TASKLET_STATE_RUN的作用。因为每个cpu都有个ksoftirqd线程,软中断和ksoftirqd线程存在竞争关系。
  1. 禁止本地中断
  2. 获得本地cpu的逻辑号n
  3. 把tasklet_vec[n]或tasklet_hi_vec[n]只想的链表的地址存入局部变量list。
  4. 把tasklet_vec[n]或tasklet_hi_vec[n]的值赋为NULL,因此已调度的tasklet描述符的链表被清空。
  5. 打开本地中断。
  6. 对于list指向的链表中的每个tasklet描述符:
a. 在多处理器系统上,检查tasklet的TASKLET_STATE_RUN标志。
    1、如果该标志被设置,说明同类型的一个tasklet正在另一个cpu上运行,因此就把任务描述符重新插入到由tasklet_vec[n]
或tasklet_hi_vec[n]指向的链表中,并再次激活TASKLET_SOFTIRQ软中断。这样,当同类型的其他tasklet在其他cpu上运行时,
这个tasklet就被延迟。
    2、如果TASKLET_STATE_RUN标志未被设置,tasklet就没有在其他cpu上运行,就需要设置这个标志,一遍tasklet函数不能
在其他cpu上执行。
b. 通过查看tasklet的count,检查tasklet是否被禁止。如果是,就清TASKLET_STATE_RUN标志,并把描述符重新插到tasklet_vec[n]
或tasklet_hi_vec[n]指向的链表中,然后函数再次激活TASKLET_SOFTIRQ软中断。
c. 如果tasklet被激活,清TASKLET_STATE_SCHED标志,并执行tasklet函数

备注:

这个可以这么分析,多个cpu共同调用tasklet_schedule()函数

软中断和tasklet的区别:

tasklet同一个tasklet在不同cpu上运行是错误的,而软中断同种软中断可以在不同cpu上运行。

三、ksoftirqd

每个处理器都有一个辅助处理软中断的内核线程。当内核中出现大量软中断的时候,这些内核进程就会辅助处理他们。

对于软中断的处理,内核会选取合适的时机处理,而在中断处理程序返回时是最常见的,这种情况下当有大量流量进设备时软中断被触发的频率可能很高。更不利的是,处理函数有时还会自行重复出发,这样会导致cpu一直被占用,导致用户空间进程无法获得足够的处理器时间,因此处饥饿状态。

为改进这种情况,当大量软中断出现的时候,内核会唤醒一组内核线程来处理这些负载。这些线程在最低的优先级上运行(nice值是19),这能避免他们跟其他重要的任务抢夺cpu,但他们最终肯定会被执行。

每个处理器都有这样一个线程,所有线程的名字都叫做ksoftirq/n,区别在于n,他对应的处理器的编号。在一个双cpu的机器上就有两个这样的线程,分别叫做ksoftirqd/0和ksoftirqd/1,为了保证只要有空闲的处理器,他们就会处理软中断,所以给每个处理器都分配一个这样的线程。一旦线程被初始化,他就会执行下面这样的死循环:

中

static int ksoftirqd(void * __bind_cpu) 
{ 
    set_user_nice(current, 19); 
    current->flags |= PF_NOFREEZE;

    set_current_state(TASK_INTERRUPTIBLE);

    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()) { 
            /* Preempt disable stops cpu going offline. 
               If already offline, we'll be on wrong CPU: 
               don't process */ 
            if (cpu_is_offline((long)__bind_cpu)) 
                goto wait_to_die; 
            do_softirq(); //调用软中断下半部处理函数
            preempt_enable_no_resched(); 
            cond_resched(); 
            preempt_disable(); 
        } 
        preempt_enable(); 
        set_current_state(TASK_INTERRUPTIBLE); 
    } 
    __set_current_state(TASK_RUNNING); 
    return 0;

wait_to_die: 
    preempt_enable(); 
    /* Wait for kthread_stop */ 
    set_current_state(TASK_INTERRUPTIBLE); 
    while (!kthread_should_stop()) { 
        schedule(); 
        set_current_state(TASK_INTERRUPTIBLE); 
    } 
    __set_current_state(TASK_RUNNING); 
    return 0; 
}

从这里可以看出,这个ksoftirqd线程是可以被调度的,同样软中断也不是在中断的上下文中,软中断也是可以调度的。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

你可能感兴趣的:(linux内核)