<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
	<channel>
		<atom:link href="http://gentoo-zh.org/extern.php?action=feed&amp;tid=834&amp;type=rss" rel="self" type="application/rss+xml" />
		<title><![CDATA[Gentoo中文社区 / Gentoo 之 IRQ subsystem 之 软件中断（softIRQ）]]></title>
		<link>http://www.gentoo-zh.org/viewtopic.php?id=834</link>
		<description><![CDATA[Gentoo 之 IRQ subsystem 之 软件中断（softIRQ） 最近发表的帖子。]]></description>
		<lastBuildDate>Sat, 16 Mar 2024 14:50:49 +0000</lastBuildDate>
		<generator>FluxBB</generator>
		<item>
			<title><![CDATA[Gentoo 之 IRQ subsystem 之 软件中断（softIRQ）]]></title>
			<link>http://www.gentoo-zh.org/viewtopic.php?pid=952#p952</link>
			<description><![CDATA[<p>软件中断（softIRQ）是内核提供的一种延迟执行机制，它完全由软件触发，虽然说是延迟机制，实际上，在大多数情况下，它与普通进程相比，能得到更快的响应时间。软中断也是其他一些内核机制的基础，比如tasklet，高分辨率timer等。</p><p>/*****************************************************************************************************/<br />声明：本博内容均由http://blog.csdn.net/droidphone原创，转载请注明出处，谢谢！<br />/*****************************************************************************************************/<br />1.&#160; 软件中断的数据结构</p><p>1.1&#160; struct softirq_action</p><p>&#160; &#160; &#160; &#160; 内核用softirq_action结构管理软件中断的注册和激活等操作，它的定义如下： </p><div class="codebox"><pre><code>struct softirq_action
{
	void	(*action)(struct softirq_action *);
};</code></pre></div><p>非常简单，只有一个用于回调的函数指针。软件中断的资源是有限的，内核目前只实现了10种类型的软件中断，它们是：</p><div class="codebox"><pre><code>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
};</code></pre></div><p>内核的开发者们不建议我们擅自增加软件中断的数量，如果需要新的软件中断，尽可能把它们实现为基于软件中断的tasklet形式。与上面的枚举值相对应，内核定义了一个softirq_action的结构数组，每种软中断对应数组中的一项： </p><div class="codebox"><pre><code>static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;</code></pre></div><p>1.2&#160; irq_cpustat_t</p><p>&#160; &#160; &#160; &#160; 多个软中断可以同时在多个cpu运行，就算是同一种软中断，也有可能同时在多个cpu上运行。内核为每个cpu都管理着一个待决软中断变量（pending），它就是irq_cpustat_t： </p><div class="codebox"><pre><code>typedef struct {
	unsigned int __softirq_pending;
} ____cacheline_aligned irq_cpustat_t;

irq_cpustat_t irq_stat[NR_CPUS] ____cacheline_aligned;</code></pre></div><p>__softirq_pending字段中的每一个bit，对应着某一个软中断，某个bit被置位，说明有相应的软中断等待处理。</p><p>1.3&#160; 软中断的守护进程ksoftirqd</p><p>&#160; &#160; &#160; &#160; 在cpu的热插拔阶段，内核为每个cpu创建了一个用于执行软件中断的守护进程ksoftirqd，同时定义了一个per_cpu变量用于保存每个守护进程的task_struct结构指针： </p><div class="codebox"><pre><code>DEFINE_PER_CPU(struct task_struct *, ksoftirqd);</code></pre></div><p>大多数情况下，软中断都会在irq_exit阶段被执行，在irq_exit阶段没有处理完的软中断才有可能会在守护进程中执行。</p><p>2.&#160; 触发软中断</p><p>&#160; &#160; &#160; &#160; 要触发一个软中断，只要调用api：raise_softirq即可，它的实现很简单，先是关闭本地cpu中断，然后调用：raise_softirq_irqoff </p><div class="codebox"><pre><code>void raise_softirq(unsigned int nr)
{
	unsigned long flags;
 
	local_irq_save(flags);
	raise_softirq_irqoff(nr);
	local_irq_restore(flags);
}</code></pre></div><p>再看看raise_softirq_irqoff：</p><div class="codebox"><pre><code>inline void raise_softirq_irqoff(unsigned int nr)
{
	__raise_softirq_irqoff(nr);
 
        ......
	if (!in_interrupt())
		wakeup_softirqd();
}</code></pre></div><p>先是通过__raise_softirq_irqoff设置cpu的软中断pending标志位（irq_stat[NR_CPUS] ），然后通过in_interrupt判断现在是否在中断上下文中，或者软中断是否被禁止，如果都不成立，则唤醒软中断的守护进程，在守护进程中执行软中断的回调函数。否则什么也不做，软中断将会在中断的退出阶段被执行。</p><p>3.&#160; 软中断的执行</p><p>&#160; &#160; &#160; &#160; 基于上面所说，软中断的执行既可以守护进程中执行，也可以在中断的退出阶段执行。实际上，软中断更多的是在中断的退出阶段执行（irq_exit），以便达到更快的响应，加入守护进程机制，只是担心一旦有大量的软中断等待执行，会使得内核过长地留在中断上下文中。</p><p>3.1&#160; 在irq_exit中执行<br />&#160; &#160; &#160; &#160; 看看irq_exit的部分： </p><div class="codebox"><pre><code>void irq_exit(void)
{
        ......
	sub_preempt_count(IRQ_EXIT_OFFSET);
	if (!in_interrupt() &amp;&amp; local_softirq_pending())
		invoke_softirq();
        ......
}</code></pre></div><p>如果中断发生嵌套，in_interrupt()保证了只有在最外层的中断的irq_exit阶段，invoke_interrupt才会被调用，当然，local_softirq_pending也会实现判断当前cpu有无待决的软中断。代码最终会进入__do_softirq中，内核会保证调用__do_softirq时，本地cpu的中断处于关闭状态，进入__do_softirq： </p><div class="codebox"><pre class="vscroll"><code>asmlinkage void __do_softirq(void)
{
        ......
	pending = local_softirq_pending();
 
	__local_bh_disable((unsigned long)__builtin_return_address(0),
				SOFTIRQ_OFFSET);
restart:
	/* Reset the pending bitmask before enabling irqs */
	set_softirq_pending(0);
 
	local_irq_enable();
 
	h = softirq_vec;
 
	do {
		if (pending &amp; 1) {
	                ......
			trace_softirq_entry(vec_nr);
			h-&gt;action(h);
			trace_softirq_exit(vec_nr);
                        ......
		}
		h++;
		pending &gt;&gt;= 1;
	} while (pending);
 
	local_irq_disable();
 
	pending = local_softirq_pending();
	if (pending &amp;&amp; --max_restart)
		goto restart;
 
	if (pending)
		wakeup_softirqd();
 
	lockdep_softirq_exit();
 
	__local_bh_enable(SOFTIRQ_OFFSET);
}</code></pre></div><p>&#160; &#160; 首先取出pending的状态；<br />&#160; &#160; 禁止软中断，主要是为了防止和软中断守护进程发生竞争；<br />&#160; &#160; 清除所有的软中断待决标志；<br />&#160; &#160; 打开本地cpu中断；<br />&#160; &#160; 循环执行待决软中断的回调函数；<br />&#160; &#160; 如果循环完毕，发现新的软中断被触发，则重新启动循环，直到以下条件满足，才退出：<br />&#160; &#160; &#160; &#160; 没有新的软中断等待执行；<br />&#160; &#160; &#160; &#160; 循环已经达到最大的循环次数MAX_SOFTIRQ_RESTART，目前的设定值时10次；<br />&#160; &#160; 如果经过MAX_SOFTIRQ_RESTART次循环后还未处理完，则激活守护进程，处理剩下的软中断；<br />&#160; &#160; 推出前恢复软中断；</p><br /><p>3.2&#160; 在ksoftirqd进程中执行<br />&#160; &#160; &#160; &#160; 从前面几节的讨论我们可以看出，软中断也可能由ksoftirqd守护进程执行，这要发生在以下两种情况下：</p><p>&#160; &#160; 在irq_exit中执行软中断，但是在经过MAX_SOFTIRQ_RESTART次循环后，软中断还未处理完，这种情况虽然极少发生，但毕竟有可能；<br />&#160; &#160; 内核的其它代码主动调用raise_softirq，而这时正好不是在中断上下文中，守护进程将被唤醒；</p><p>守护进程最终也会调用__do_softirq执行软中断的回调，具体的代码位于run_ksoftirqd函数中，内核会关闭抢占的情况下执行__do_softirq，具体的过程这里不做讨论。</p><p>4.&#160; tasklet</p><p>&#160; &#160; &#160; &#160;因为内核已经定义好了10种软中断类型，并且不建议我们自行添加额外的软中断，所以对软中断的实现方式，我们主要是做一个简单的了解，对于驱动程序的开发者来说，无需实现自己的软中断。但是，对于某些情况下，我们不希望一些操作直接在中断的handler中执行，但是又希望在稍后的时间里得到快速地处理，这就需要使用tasklet机制。 tasklet是建立在软中断上的一种延迟执行机制，它的实现基于TASKLET_SOFTIRQ和HI_SOFTIRQ这两个软中断类型。</p><p>4.1&#160; tasklet_struct&#160; &#160; &#160; &#160; </p><p>在软中断的初始化函数softirq_init的最后，内核注册了TASKLET_SOFTIRQ和HI_SOFTIRQ这两个软中断：</p><div class="codebox"><pre><code>void __init softirq_init(void)
{
        ......
	open_softirq(TASKLET_SOFTIRQ, tasklet_action);
	open_softirq(HI_SOFTIRQ, tasklet_hi_action);
}</code></pre></div><p>内核用一个tasklet_struct来表示一个tasklet，它的定义如下： </p><div class="codebox"><pre><code>struct tasklet_struct
{
	struct tasklet_struct *next;
	unsigned long state;
	atomic_t count;
	void (*func)(unsigned long);
	unsigned long data;
};</code></pre></div><p>next用于把同一个cpu的tasklet链接成一个链表，state用于表示该tasklet的当前状态，目前只是用了最低的两个bit，分别用于表示已经准备被调度执行和已经在另一个cpu上执行： </p><div class="codebox"><pre><code>enum
{
	TASKLET_STATE_SCHED,	/* Tasklet is scheduled for execution */
	TASKLET_STATE_RUN	/* Tasklet is running (SMP only) */
};</code></pre></div><p>原子变量count用于tasklet对tasklet_disable和tasklet_enable的计数，count为0时表示允许tasklet执行，否则不允许执行，每次tasklet_disable时，该值加1，tasklet_enable时该值减1。func是tasklet被执行时的回调函数指针，data则用作回调函数func的参数。 </p><p>4.2&#160; 初始化一个tasklet</p><p>有两种办法初始化一个tasklet，第一种是静态初始化，使用以下两个宏，这两个宏定义一个tasklet_struct结构，并用相应的参数对结构中的字段进行初始化：</p><p>&#160; &#160; DECLARE_TASKLET(name, func, data)；定义名字为name的tasklet，默认为enable状态，也就是count字段等于0。<br />&#160; &#160; DECLARE_TASKLET_DISABLED(name, func, data)；定义名字为name的tasklet，默认为enable状态，也就是count字段等于1。</p><p>第二个是动态初始化方法：先定义一个tasklet_struct，然后用tasklet_init函数进行初始化，该方法默认tasklet处于enable状态： </p><div class="codebox"><pre><code>struct tasklet_struct tasklet_xxx;
......
tasklet_init(&amp;tasklet_xxx, func, data);</code></pre></div><p>4.3&#160; tasklet的使用方法</p><p>使能和禁止tasklet，使用以下函数：</p><p>&#160; &#160; tasklet_disable()&#160; 通过给count字段加1来禁止一个tasklet，如果tasklet正在运行中，则等待运行完毕才返回（通过TASKLET_STATE_RUN标志）。<br />&#160; &#160; tasklet_disable_nosync()&#160; tasklet_disable的异步版本，它不会等待tasklet运行完毕。<br />&#160; &#160; tasklet_enable()&#160; 使能tasklet，只是简单地给count字段减1。</p><p>调度tasklet的执行，使用以下函数：</p><p>&#160; &#160; tasklet_schedule(struct tasklet_struct *t)&#160; 如果TASKLET_STATE_SCHED标志为0，则置位TASKLET_STATE_SCHED，然后把tasklet挂到该cpu等待执行的tasklet链表上，接着发出TASKLET_SOFTIRQ软件中断请求。<br />&#160; &#160; tasklet_hi_schedule(struct tasklet_struct *t)&#160; 效果同上，区别是它发出的是HI_SOFTIRQ软件中断请求。</p><p>销毁tasklet，使用以下函数：</p><p>&#160; &#160; tasklet_kill(struct tasklet_struct *t)&#160; 如果tasklet处于TASKLET_STATE_SCHED状态，或者tasklet正在执行，则会等待tasklet执行完毕，然后清除TASKLET_STATE_SCHED状态。</p><br /><p>4.4&#160; tasklet的内部执行机制</p><p>内核为每个cpu用定义了一个tasklet_head结构，用于管理每个cpu上的tasklet的调度和执行： </p><div class="codebox"><pre><code>struct tasklet_head
{
	struct tasklet_struct *head;
	struct tasklet_struct **tail;
};
 
static DEFINE_PER_CPU(struct tasklet_head, tasklet_vec);
static DEFINE_PER_CPU(struct tasklet_head, tasklet_hi_vec);</code></pre></div><p>回到4.1节，我们知道，tasklet是利用TASKLET_SOFTIRQ和HI_SOFTIRQ这两个软中断来实现的，两个软中断只是有优先级的差别，所以我们只讨论TASKLET_SOFTIRQ的实现，TASKLET_SOFTIRQ的中断回调函数是tasklet_action，我们看看它的代码： </p><div class="codebox"><pre class="vscroll"><code>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, &amp;__get_cpu_var(tasklet_vec).head);
	local_irq_enable();
 
	while (list) {
		struct tasklet_struct *t = list;
 
		list = list-&gt;next;
 
		if (tasklet_trylock(t)) {
			if (!atomic_read(&amp;t-&gt;count)) {
				if (!test_and_clear_bit(TASKLET_STATE_SCHED, &amp;t-&gt;state))
					BUG();
				t-&gt;func(t-&gt;data);
				tasklet_unlock(t);
				continue;
			}
			tasklet_unlock(t);
		}
 
		local_irq_disable();
		t-&gt;next = NULL;
		*__this_cpu_read(tasklet_vec.tail) = t;
		__this_cpu_write(tasklet_vec.tail, &amp;(t-&gt;next));
		__raise_softirq_irqoff(TASKLET_SOFTIRQ);
		local_irq_enable();
	}
}</code></pre></div><p>解析如下：</p><p>&#160; &#160; 关闭本地中断的前提下，移出当前cpu的待处理tasklet链表到一个临时链表后，清除当前cpu的tasklet链表，之所以这样处理，是为了处理当前tasklet链表的时候，允许新的tasklet被调度进待处理链表中。<br />&#160; &#160; 遍历临时链表，用tasklet_trylock判断当前tasklet是否已经在其他cpu上运行，而且tasklet没有被禁止：<br />&#160; &#160; &#160; &#160; 如果没有运行，也没有禁止，则清除TASKLET_STATE_SCHED状态位，执行tasklet的回调函数。<br />&#160; &#160; &#160; &#160; 如果已经在运行，或者被禁止，则把该tasklet重新添加会当前cpu的待处理tasklet链表上，然后触发TASKLET_SOFTIRQ软中断，等待下一次软中断时再次执行。</p><p>分析到这了我有个疑问，看了上面的代码，如果一个tasklet被tasklet_schedule后，在没有被执行前被tasklet_disable了，岂不是会无穷无尽地引发TASKLET_SOFTIRQ软中断？<br />通过以上的分析，我们需要注意的是，tasklet有以下几个特征：</p><p>&#160; &#160; 同一个tasklet只能同时在一个cpu上执行，但不同的tasklet可以同时在不同的cpu上执行；<br />&#160; &#160; 一旦tasklet_schedule被调用，内核会保证tasklet一定会在某个cpu上执行一次；<br />&#160; &#160; 如果tasklet_schedule被调用时，tasklet不是出于正在执行状态，则它只会执行一次；<br />&#160; &#160; 如果tasklet_schedule被调用时，tasklet已经正在执行，则它会在稍后被调度再次被执行；<br />&#160; &#160; 两个tasklet之间如果有资源冲突，应该要用自旋锁进行同步保护；</p>]]></description>
			<author><![CDATA[dummy@example.com (batsom)]]></author>
			<pubDate>Sat, 16 Mar 2024 14:50:49 +0000</pubDate>
			<guid>http://www.gentoo-zh.org/viewtopic.php?pid=952#p952</guid>
		</item>
	</channel>
</rss>
