下部分
背景
中断“上部分的缺陷
1.中断处理程序以异步的方式进行,并且它有可能打断其他重要的代码(甚至可能打断其他中断处理程序)的执行,因此,为了被打断的代码停止时间过长,中断处理程序应该执行越快越好。
2.·如果当前有一个中断处理程序正在执行,在最好的情况下(如果IRQF_DISABLED设有被设置),与该中断同级的其他中断会被屏蔽,在最坏的情况下(如果设置了IRQF_DISABLED),当前处理器上所有其他中断都会被屏蔽。因为禁止中断后硬件与操作系统无法通信,因此,中断处理程序执行得越快越好。
3.·由于中断处理程序往往需要对硬件进行操作,所以它们通常有很高的时限要求。
4.·中断处理程序不在进程上下文中运行,所以它们不能阻塞。这限制了它们所做的事情。现在,为什么中断处理程序只能作为整个硬件中断处理流程一部分的原因就很明显了。操作系统必须有一个快速、异步、简单的机制负责对硬件做出迅速响应并完成那些时间要求很严格的操作。中断处理程序很适合于实现这些功能,可是,对于那些其他的、对时间要求相对宽松的任务,就应该推后到中断被激活以后再去运行。
下部分是什么
下半部的任务就是执行与中断处理密切相关但中断处理程序本身不执行的工作。在理想的情况下,最好是中断处理程序将所有工作都交给下半部分执行,因为我们希望在中断处理程序中完成的工作越少越好(也就是越快越好)。我们期望中断处理程序能够尽可能快地返回。
借鉴情况:
·如果一个任务对时间非常敏感,将其放在中断处理程序中执行。·
如果一个任务和硬件相关,将其放在中断处理程序中执行。
·如果一个任务要保证不被其他中断(特别是相同的中断)打断,将其放在中断处理程序中执行。
·其他所有任务,考虑放置在下半部执行。
怎么实现linux”下部分”
概要
下半部分机制 |
状态 |
BH | 去除 |
任务队列 | 去除 |
软中断 | 引入 |
tasklet | 引入 |
工作队列 | 映入 |
软中断
先看源代码
struct softirq_action
{
void (*action)(struct softirq_action *);
};
static struct softirq_action softirq_vec[NR-SOFTIRQS]
//定义32
1.软件中断程序
当内核运行一个软中断处理程序的时候,它就会执行这个action 函数,其唯一的参数为指向相应softirq_action结构体的指针。例如,如果my_softirq指向softirq_vec数组的某项,那么内核会用如下的方式调用软中断处理程序中的函数:
my_softirq->action(my_softirq);
当你看到内核把整个结构体都传递给软中断处理程序而不是仅仅传递数据值的时候,你可能会很吃惊。这个小技巧可以保证将来在结构体中加入新的域时,无须对所有的软中断处理程序都进行变动。如果需要,软中断处理程序可以方便地解析它的参数,从数据成员中提取数值。
一-个软中断不会抢占另外一个软中断。实际上,唯一-可以抢占软中断的是中断处理程序。不过,其他的软中断(甚至是相同类型的软中断)可以在其他处理器上同时执行。
2.执行软中断
一个注册的软中断必须在被标记后才会执行。这被称作触发软中断(raising the softirq )。通常,中断处理程序会在返回前标记它的软中断,使其在稍后被执行。于是,在合适的时刻,该软由断读会运行。在下列地方,待外理的软中断会被桧杏和执行﹐
1 从一个硬件中断代码出
2 在ksoftirq内核线程中
3 在那些显示检查和待处理的软件中端代码处理
软件中断都要从do_soft函数中执行
u32 pending;
pending=local_softirq_pending();
if(pending)
{
struct softirq_action*h;
/* 设置带处理位图*/
set_softirq_pending(0)
h=softirq_vec;
do{
if(pending&1)
h->action(h);
h++
pending>>=1;
}
while(pending);
}
1)用局部变量pending保存local_softirq _pending()宏的返回值。它是待处理的软中断的32位位图——如果第n位被设置为1,那么第n位对应类型的软中断等待处理。
2〉现在待处理的软中断位图已经被保存,可以将实际的软中断位图清零了。
3)将指针h指向softirq_vec的第一项。
4)如果pending的第一位被置为1,则h->action(h)被调用。
5)指针加1,所以现在它指向softirq_vec数组的第二项。
6)位掩码pending右移一位。这样会丢弃第一位,然后让其他各位依次向右移动一个位置。于是,原来的第二位现在就在第一位的位置上了(依次类推)。
7)现在指针h指向数组的第二项,pending位掩码的第二位现在也到了第一位上。重复执行上面的步骤。
8)一直重复下去,直到pending 变为0,这表明已经没有待处理的软中断了,我们的任务也就完成了。注意,这种检查足以保证h总指向softirq_vec的有效项,因为pending最多只可能设置32位,循环最多也只能执行32次。
tasklet
tasklet是利用软中断实现的一种下半部机制。我们之前提到过,它和进程没有任何关系。tasklet和软中断在本质上很相似,行为表现也相近,但是,它的接口更简单,锁保护也要求较低。
选择到底是用软中断还是tasklet其实很简单:通常你应该用tasklet。就像我们在前面看到的,软中断的使用者屈指可数。它只在那些执行频率很高和连续性要求很高的情况下才需要使用。而tasklet却有更广泛的用途。大多数情况下用tasklet效果都不错,而且它们还非常容易使用。
tasklet的实现
因为tasklet是通过软中断实现的,所以它们本身也是软中断。,tasklet由两类软中断代表:HI_SOFTIRQ和TASKLET_SOFTIRQ。这两者之间唯一的实际区别在于,HI_SOFTIRQ类型的软中断先于TASKLET_SOFTIRQ类型的软中断执行。
1.taskle结构体
struct tasklet_struct
{
struct tasklet_struct *next;
unsigned long state;
atomic_t count;
void (*func)(unsigned long);
unsigned long data;
};
结构体中的func成员是tasklet 的处理程序(像软中断中的action一样), data是它唯一的参数。
state成员只能在0、TASKLET_STATE_SCHED和TASKLET_STATE_RUN之间取值。TASKLET_STATE_SCHED表明tasklet已被调度,正准备投入运行,TASKLET_STATE_RUN表明该tasklet 正在运行。TASKLET_STATE_RUN 只有在多处理器的系统上才会作为–种优化来使用,单处理器系统任何时候都清楚单个tasklet是不是正在运行(它要么就是当前正在执行的代码,要么不是)。
count成员是tasklet的引用计数器。如果它不为0,则tasklet被禁止,不允许执行﹔只有当它为0时,tasklet才被激活,并且在被设置为挂起状态时,该tasklet才能够执行。
调度tasklet
已调度的tasklet(等同于被触发的软中断)存放在两个单处理器数据结构: tasklet_vec(普通tasklet)和 tasklet_hi_vec(高优先级的tasklet)。这两个数据结构都是由tasklet_struct结构体构成的链表。链表中的每个tasklet struct代表一个不同的tasklet。
tasklet由 tasklet_schedule()和 tasklet_hi_schedule()函数进行调度,它们接受一个指向tasklet_struct结构的指针作为参数。两个函数非常类似(区别在于一个使用TASKLET_SOFTIRQ而另一个用HI_SOFTIRQ)。在接下来的内容中我们将仔细研究怎么编写和使用taskiets。现在,让我们先考察一下tasklet schedule(的细节
tasklet_schedule:
1)检查tasklet的状态是否为TASKLET_STATE_SCHED。如果是,说明tasklet已经被调度过了9,函数立即返回。
2)调用_tasklet_schedule()。
3)保存中断状态,然后禁止本地中断。在我们执行tasklet代码时,这么做能够保证当tasklet_schedule()处理这些tasklet时,处理器上的数据不会弄乱。
4)把需要调度的tasklet加到每个处理器一个的 tasklet_vec链表或tasklet_hi_vec链表的表头上去。
5)唤起TASKLET_SOFTIRQ或HI_SOFTIRQ软中断,这样在下一次调用do_softirq()时就会执行该tasklet。
6)恢复中断到原状态并返回。
]tasklet的实现很简单,但非常巧妙。我们可以看到,所有的tasklet都通过重复运用HI_SOFTIRQ和TASKLET_SOFTIRQ这两个软中断实现。当一个tasklet被调度时,内核就会唤起这两个软中断中的一个。随后,该软中断会被特定的函数处理,执行所有已调度的 tasklet。这个函数保证同一时间里只有一个给定类别的tasklet 会被执行(但其他不同类型的tasklet可以同时执行)。所有这些复杂性都被一个简洁的接口隐藏起来了。
工作队列
工作队列(work queue)是另外一种将工作推后执行的形式,它和我们前面讨论的所有其他形式都不相同。工作队列可以把工作推后,交由一个内核线程去执行—-这个下半部分总是会在进程上下文中执行。这样,通过工作队列执行的代码能占尽进程上下文的所有优势。最重要的就是工作队列允许重新调度甚至是睡眠。
如何选择tasklet/softirq
通常,在工作队列和软中断/tasklet中做出选择非常容易。如果推后执行的任务需要睡眠,那么就选择工作队列。如果推后执行的任务不需要睡眠,那么就选择软中断或taskilet。实际上,工作队列通常可以用内核线程替换。但是由于内核开发者们非常反对创建新的内核线程(在有些场合,使用这种冒失的方法可能会吃到苦头),所以我们也推荐使用工作队列。当然,这种接口也的确很容易使用。
如果你需要用一个可以重新调度的实体来执行你的下半部外理,你应该使用工作队列。它是
唯一能在进程上下文中运行的下半部实现机制,也只有它才可以睡眠。这意味着在你需要获得大量的内存时,在你需要获取信号量时,在你需要执行阻塞式的UO操作时,它都会非常有用。如果你不需要用一个内核线程来推后执行工作,那么就考虑使用tasklet吧。
工作队列的实现
工作队列子系统是一个用于创建内核线程的接口,通过它创建的进程负责执行由内核其他部分排到队列里的任务。它创建的这些内核线程称作工作者线程(worker thread)。工作队列可以让你的驱动程序创建一个专门的工作者线程来处理需要推后的工作。不过,工作队列子系统提供了一个缺省的工作者线程来处理这些工作。因此,工作队列最基本的表现形式,就转变成了一个把需要推后执行的任务交给特定的通用线程的这样一种接口。
缺省的工作者线程叫做events/n,这里n是处理器的编号﹔每个处理器对应一个线程。例如,单处理器的系统只有events/0这样一个线程,而双处理器的系统就会多一个events/1线程。缺省的工作者线程会从多个地方得到被推后的工作。许多内核驱动程序都把它们的下半部交给缺省的工作者线程去做。除非一个驱动程序或者子系统必须建立一个属于它自己的内核线程,否则最好使用缺省线程。
struct workqueue_struct
{
struct cpu_workqueu_struct cpu_wq(NR_CPUS);
struct list_head list;
const char* name;
int singlethread;
int freezeable;
int rt;
};
该结构内是一个由cpu_workqueue_struct结构组成的数组,它定义在kernel/workqueue.c中,数组的每一项对应系统中的–个处理器。由于系统中每个处理器对应一个工作者线程,所以对于给定的某台计算机来说,就是每个处理器,每个工作者线程对应一个这样的cpu_workqueue_struct结构体。cpu_work queue_struct是 kernel/workqueue.c中的核心数据结构:
struct cpu_workqueue_struct
{
spinlock_t lock;
struct list_head worklist;
wait_queue_head_t more_Work;
struct work_struct*current_struct;
struct workqueue_struct *wq;
task_t *thread
};
注意,每个工作者线程类型关联一个自己的workqueue_struct。在该结构体里面,给每个线程分配–个cpu_workqueue_struct,因而也就是给每个处理器分配一个,因为每个处理器都有一个该类型的工作者线程。
2.表示工作的数据结构
所有的工作者线程都是用普通的内核线程实现的,它们都要执行worker_thread()函数。在它初始化完以后,这个函数执行一个死循环并开始休眠。当有操作被插入到队列里的时候,线程就会被唤醒,以便执行这些操作。当没有剩余的操作时,它又会继续休眠。
struct work_struct
{
atomic_long_t data;
struct list_head entry;
work_func_t func;
};
这些结构体被连接成链表,在每个处理器上的每种类型的队列都对应这样一个链表。比如,每个处理器上用于执行被推后的工作的那个通用线程就有一个这样的链表。当一个工作者线程被唤醒时,它会执行它的链表上的所有工作。工作被执行完毕,它就将相应的work_struct对象从链表上移去。当链表上不再有对象的时候,它就会继续休眠。
我们可以看一下 work_thread()的核心流程:
for(;;)
{
prepare_to_wait(&cwq->more.&wait,TASK_INTERRUPTIBLE);
if(list->empty)
schedule();
finish_wait((&cwq->more.&wait);
run_workqueue(cwq);
}
1)线程将自己设置为休眠状态( state被设成TASK_INTERRUPTIBLE),并把自己加入到亭待队列中。
2)如果工作链表是空的,线程调用schedule()函数进入睡眠状态。
3)如果链表中有对象,线程不会睡眠。相反,它将自己设置成TASK_RUNNING,脱离等寺队列。
4)如果链表非空,调用run_workqueue(函数执行被推后的工作。
while(list_empty(&cwq->worklist))
{
struct work_struct *work;
work_fun_t f;
void* data;
work=list_entry(cwq->worklist.next,struct work_struct entry);
f=work->fun;
list_del_init(cwq->worklist.next);
work_clear_pending(work);
f(work);
}
1)当链表不为空时,选取下一个节点对象。
2)获取我们希望执行的函数func及其参数data。
3)把该节点从链表上解下来,将待处理标志位pending 清零。
4)调用函数。
5)重复执行。
工作队列总结
位于最高一层的是工作者线程。系统允许有多种类型的工作者线程存在。对于指定的一个类型,系统的每个CPU上都有一个该类的工作者线程。内核中有些部分可以根据需要来创建工作者线程,而在默认情况下内核只有event这一种类型的工作者线程。每个工作者线程都由一个cpu_workequeue_struct结构体表示。而workqueue_struct结构体则表示给定类型的所有工作者线程。
工作处于最底层,让我们从这里开始。你的驱动程序创建这些需要推后执行的工作。它们用work_struct结构来表示。这个结构体中最重要的部分是一个指针,它指向一个函数,而正是该函数负责处理需要推后执行的具体任务。工作会被提交给某个具体的工作者线程―-在这种情况下,就是特殊的falcon线程。然后这个工作者线程会被唤醒并执行这些排好的工作。
中断下部分机制
从设计的角度考虑,软中断提供的执行序列化的保障最少。这就要求软中断处理函数必须格外小心地采取一些步骤确保共享数据的安全,两个甚至更多相同类别的软中断有可能在不同的处理器上同时执行。如果被考察的代码本身多线索化的工作就做得非常好,比如网络子系统,它完全使用单处理器变量,那么软中断就是非常好的选择。对于时间要求严格和执行频率很高的应用来说,它执行得也最快。
如果代码多线索化考虑得并不充分,那么选择tasklet意义更大。它的接口非常简单,而且,由于两个同种类型的tasklet 不能同时执行,所以实现起来也会简单一些。tasklet是有效的软中断,但不能并发运行。驱动程序开发者应当尽可能选择tasklet而不是软中断,当然,如果准备利用每一处理器上的变量或者类似的情形,以确保软中断能安全地在多个处理器上并发地运行,那么还是选择软中断。
如果你需要把任务推后到进程上下文中完成,那么在这三者中就只能选择工作队列了。如果进程上下文并不是必须的条件(明确点说,就是如果并不需要睡眠),那么软中断和 tasklet可能更合适。工作队列造成的开销最大,因为它要牵扯到内核线程甚至是上下文切换。这并不是说工作队列的效率低,如果每秒钟有几千次中断,就像网络子系统时常经历的那样,那么采用其他的机制可能更合适一些。尽管如此,针对大部分情况,工作队列都能提供足够的支持。
下部分 | 上下文 | 顺序执行保障 |
软中断 | 中断 | 没有 |
tasklet | 中断 | 同种类型的tasklet不能同时执行 |
任务队列 | 进程 | 没有 |
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/129644.html