向下之旅(十):下半部和推后执行的工作(一)

  中断处理程序是内核中很有用的——实际上也是必不可少的一部分。但是一些局限导致中断处理程序只能完成中断处理流程的上半部分。这些局限包括:

  1.中断处理程序以异步的方式执行,并有可能打算其他重要代码(甚至是其他中断处理程序)的执行。因此,为了避免被打断的代码停止时间过长,中断处理程序应该执行的越快越好。

  2.如果当前有一个中断处理程序正在执行,最好的情况下(如果设置了SA_INTERRUPT),与该中断同级的其他中断会被屏蔽,在最坏的情况下,当前处理器上所有其他中断都会被屏蔽。因此,仍应该让它们执行的越快越好。

  3.由于中断处理程序往往需要对硬件进行操作,所以它们通常有很高的时限要求。

  4.中断处理程序不在进程上下文中运行,所以它们不能阻塞。这限制了它们所做的事情。

  因此,整个中断处理流程就被分了两个部分,或叫两半,第一部分是中断处理程序(上半部),就像我们在上一章讨论的那样,内核通过对它的异步执行完成对硬件中断的即使响应。对于那些其他的,对时间要求相对宽松的任务,就应该推后到中断被激活以后再去运行,称为下半部。

  下半部

  下半部的任务就是执行与中断处理密切相关但是中断处理程序本身不执行的工作。理想情况下,最好是中断处理程序将所有工作都交给下半部执行。对于上半部和下半部之间的工作划分,尽管不存在某种严格的规则,但是有一些提示可以借鉴:

  1.如果一个任务对时间非常敏感,将其中断处理程序中执行。

  2.如果一个任务和硬件相关,将其放在中断处理程序中执行。

  3.如果一个任务要保证不被其他中断(特别是相同的中断)打断,将其放在中断处理程序中执行。

  4.其他所有任务,考虑放置在下半部执行。

  中断运行的时候,当前中断线在所有的处理器上都会被屏蔽,如果被设置为SA_INTERRUPT,则当它执行的时候会屏蔽所有的中断,所有要求中断处理程序要尽可能的简单,快速的完成,将对时限要求不高的工作放到下半部执行,提高系统的响应能力,通常下半部咋中断处理程序一返回就会马上执行,关键在于下半部执行的时候,允许响应所有的中断。

  中断处理程序可以的实现机制只有一种(中断处理程序跟上半部等价),而下半部的实现可以通过不同的接口和子系统组成,实现的方式不唯一。Linux的提供的机制一般有软中断(这里的软中断与实现系统调用所提到的软中断(准确的说叫它软件中断)指的不是同一概念)、tasklet和工作队列。

  软中断:允许在多个处理器上同时执行——即使两个类型相同也可以。必须在编译期间就进行静态注册。

  tasklet:两个不同类型的tasklet可以在不同的处理器上同时执行,但类型相同的tasklet不能同时执行。可以通过代码进行动态注册,通过软中断实现。

  软中断

  软中断使用的比较少,而tasklet是下半部更常用的一种形式。不过,tasklet是通过软中断实现的。

  软中断的实现

  软中断是在编译期间静态分配的。它不像tasklet那样能动态注册或去除。软中断由softirq_action结构表示,它定义在<linux/interrupt.h>中:

每个被注册的软中断都占据该数组的一项。因此最多可能有32个软中断。注意,这是一个定值——注册的软中断数目的最大值没法动态改变。当前版本的内核中,这32个项中只用到6个。

  软中断处理程序action的函数原型如下:

  void softirq_handler(struct softirq_action *)

  当内核运行一个软中断处理程序的时候,它就会执行这个函数,参数为指向这个软中断处理程序结构的指针。

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

  1.从一个硬件中断代码处返回时

  2.在kosftirqd内核线程中

  3.在那些显示检查和执行待处理的软中断的代码中,如网络子系统中

  当软中断被唤醒,都会在do_softirq()中执行。该函数很简单,如果有待处理的软中断,do_softirq()会循环遍历每一个,调用它们的处理程序。

  使用软中断

  软中断保留给系统中对时间要求最严格以及最重要的下半部使用。目前,只有两个子系统——网络和SCSI——直接使用软中断。此外,内核定时器和tasklet都是建立在软中断上的。若想加入一个新的软中断,首先应尝试tasklet实现。

建立一个新的软中断必须在此枚举类型中加入新的项。而加入时,不能像咋其他地方一样,简单的把新项加到队表的末尾。相反,要给你新中断分配一个合适的优先级。新项可能插在网络相关的那些项之后。

  接着,在运行时通过调用open_softirq()注册软中断处理程序,该函数有三个参数:软中断的索引号、处理函数和data域存放的数值。例如网络子系统,通过以下方式注册自己的软中断:

  open_softirq(NET_TX_SQFTIEQ, nex_tx_action, NULL);

  open_softirq(NET_RX_SQFTIEQ, nex_tx_action, NULL);

  软中断处理程序执行的时候,允许响应中断,但它自己不能休眠。在一个处理程序运行的时候,当前处理器上的软中断被禁止。但其他的处理器上仍可以执行别的软中断。这就有可能造成数据的不安全,也是为什么tasklet更受欢迎的原因。如果不需要扩展到多个处理器,那么就使用tasklet吧。

  当在枚举类型的列表中添加新项以及调用open_softirq()进行注册以后,新的软中断处理程序就能运行,通过raise_softirq()函数可以将一个软中断设置为挂起状态,让它在下次调用do_softirq()函数时投入运行。

  过程:在中断处理程序中触发软中断是最常见的形式。在这种情况下,中断处理程序执行硬件设备的相关操作,然后触发相应的软中断,最后退出。内核在执行完中断处理程序后,马上就会调用do_softirq()函数。于是软中断开始执行中断处理程序留给它完成的剩余任务。基本上就是上半部和下半部的分工。

  tasklet

  如果不需要执行频率很高和连续性要求很高的情况下,应使用tasklet而不是软中断,它是轻量级的软中断,非常容易使用。

  HI_SOFTIRQ和TASKLET_SOFTIRQ是两类tasklet中断的代表,前者优先于后者执行。

  tasklet由tasklet_struct结构表示。每一个结构体单独代表一个tasklet。

  

  结构体中的func成员是tasklet的处理程序(像软中断的action一样),data是它唯一的参数。

  state成员只能在0,TAKSLET_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_vec(普通tasklet)和tasklet_hi_vec(高优先级的tasklet)中。这两个数据结构都是由tasklet_struct结构体构成的链表。链表中的每一项都是一个tasklet。

  tasklet由tasklet_schedule()和tasklet_hi_schedule()函数进行调度。它们接受一个只想tasklet_struct结构的指针作为参数。两个函数非常类似(一个使用TASKLET_SOFTIRQ而另一个用HI_SOFTIRQ)。调度细节如下:

  1.检查tasklet是否是TASKLET_STATE_SCHED,如果是说明已经被调度(有可能是已经被调度但是还未执行时,又被唤醒了一次)函数立即返回。

  2.保存中断状态,禁止本地中断。保证在执行tasklet代码的时候,数据是安全的。

  3.把需要调度的tasklet加到每个处理器一个的tasklet_vec或者tasklet_hi_vec链表的表头上去。

  4.通过raise_softirq()唤起TASKLET_SOFTIRQ或HI_SOFTIRQ软中断。这样在下次调用do_softirq()时就会执行该tasklet。

  5.恢复中断到原状态并返回。

  do_softirq()会执行相应的软中断处理程序。而这两个处理程序,tasklet_action()和tasklet_hi_action(),就是tasklet处理的核心。主要操作如下:

  1.禁用中断,(没必要首先保存其状态,因为这里的代码总是作为软中断被调用,而且中断总是被激活的)并为当前处理器检索tasklet_vec或tasklet_hi_vec链表。

  2.将当前处理器上的该链表设置为NULL,达到清空的效果。

  3.运行响应中断,没有必要再恢复它们回原状态,因为这段程序本身就是作为软中断处理程序被调用的,所以中断是应该被允许执行的。

  4.循环遍历获得链表上的每一个待处理的tasklet。

  5.如果是多处理器系统,通过检查TASKLET_STATE_RUN状态标志来判断这个tasklet是否正在其他处理器上运行。如果它正在运行,那么现在就不要执行,跳到下一个待处理的tasklet去(同一个时间,相同类型的tasklet只能有一个运行)。

  6.如果当前这个tasklet没有执行,将其状态标志设置为TASKLET_STATE_RUN,这样别的处理器就不会再去执行它了。

  7.检查count值是否为0,确保tasklet没有被禁止。如果tasklet被禁止,则跳到下一个挂起的tasklet中去。

  8.我们已经清楚的知道这个tasklet没有在其他地方执行,并且被我们设置为执行状态,保证不会再其他处理器上被执行,并且引用计数为0,现在就可以执行tasklet的处理程序了。

  9.tasklet执行完毕,清楚tasklet的state域的TASKLET_STATE_RUN状态标志。

  10.重复以上步骤执行下一个tasklet,直到没有剩余的等待处理的tasklet。

  使用tasklet

  大多数情况下,tasklet机制都是事先你自己的下半部的最佳选择,既可以静态的创建tasklet,也可以动态的创建它。选择哪种方式取决于你到底是(或者是想要)一个对tasklet的直接引用还是间接引用,如果是静态,则通过以下宏中的一个:

  DECLARE_TASKLET(name, func , data)

  DECLARE_TRASKLET_DISABLED(name, func , data);

  这两个宏都能根据给定的名称静态的创建一个tasklet_struct结构。当该tasklet被调度以后,给定的函数func会被执行,它的参数由data给出。两者的区别在于引用计数器的初始值设置不同。前者设为0,处于激活状态。后者设为1,处于禁止状态。等价于:

  struct tasklet_struct my_tasklet = { NULL, 0, ATOMIC_INIT(0), my_tasklet_handler, dev};

  这样就创建了一个名为my_tasklet,处理程序为tasklet_handler并且已被激活的tasklet。当处理程序被调用的时候,dev就会被传递给它。

  还可以通过将一个间接引用(一个指针)赋给一个动态创建的tasklet_struct结构的方式来初始化一个tasklet:

  tasklet_init(t , tasklet_handler, dev)  /* 动态而不是静态创建 */

  tasklet处理程序必须符合规定的函数类型:

  void tasklet_handler(unsigned long data)

  因为是靠软中断实现的,所以tasklet不能睡眠。所以不能在tasklet中使用信号量或者其他什么阻塞式函数。由于tasklet运行时允许响应中断,所以必须做好预防工作(比如屏蔽中断并获取一个锁),如果你的tasklet和中断处理程序之间共享了某些数据的话(中断不允许嵌套,有可能被其他中断打断)。如果你的tasklet和其他的tasklet或者是软中断共享了数据,你必须进行适当的锁保护。

  通过调用tasklet_schdule()函数并传递给它相应的tasklet_struct的指针,该tasklet就会被调度以便执行:

  tasklet_schedule(&my_tasklet)  /* 把 my_tasklet 标记为挂起 */

  在tasklet被调度以后,只要有机会它就尽可能的早的运行,如果在它没运行之前,一个相同的takslet又被调度了(这里应该是唤起的意思,在前面讲述调度流程的小节里可以看到,调度tasklet的第一个步骤就是检查是否重复,所以这里根本不会完成调度)那么它仍然只会运行一次。若它已经开始运行,比如说在另一个处理器上,那么这个新的tasklet会被重新调度并再次运行。作为优化,一个tasklet总在调度它的处理器上执行——这能更好的利用处理器的高速缓存。

  你可以调用tasklet_disable()函数来禁止耨个指定的tasklet。如果该tasklet当前正在执行,这个函数会等到它执行完毕再返回。或者调用tasklet_disable_nosync()函数,此函数无需等待该tasklet是否在运行,但是通常这样不安全。

  调用tasklet_enable()函数可以激活一个tasklet,如果希望激活DECLARE_TASKLET_DISABLED()创建的tasklet,你也得调用这个函数。

  调用taaklet_kill()函数从挂起的队列中去掉一个taskelt。该函数的参数是一个指向某个tasklet的tasklet_struct的长指针。该函数会等到该tasklet执行完毕,然后再将它移出。不过,无法阻止其他地方的代码重新调度该tasklet,由于该函数可能会引起休眠,所以禁止在中断上下文中使用它。

  ksoftirqd

  每个处理器都有一组辅助处理软中断(和tasklet)的内核线程,当内核出现大量软中断的时候,这些内核进程就会辅助的处理它们。对于软中断,内核会选择在几个特殊的时机进行处理,而在中断处理程序返回时处理是最常见的。软中断的触发有时频率很高,有的甚至自己重复触发(网络子系统),如果没有有效的机制处理这些软中断,就会导致用户进程获得处理器的时间变少,常处于饥饿状态,让人无法接受。于是内核会唤醒一组内核进程来处理这些负载。这些线程在最低的优先级上运行(nice值是19),保证了软中断最终能够得到处理,也不会抢占用户空间进程的处理器时间。

 

  参考自:《Linux Kernel Development》.

posted on 2016-03-18 16:49  画家丶  阅读(252)  评论(0编辑  收藏  举报