您好、欢迎来到现金彩票网!
当前位置:21点 > 子队列 >

linux 中断-上半部和下半部(softirqtaskletworkqueue)

发布时间:2019-07-10 10:17 来源:未知 编辑:admin

  嗨,嗨,如果您记性好的话,我在上一篇博客中提到过这样一件事:中断处理是分为两个部分:中断处理程序是上半部,它接收到一个中断,就立即执行,但只做有严格时限的工作;而另外被叫做下半部的另外一个部分主要做被允许能稍后完成的工作。这个下半部正是今天的重点。

  下半部的任务就是执行与中断处理密切相关但中断处理程序本生身不执行的任务。最好情况当然是中断处理程序把所有的工作都交給下半部执行,而自己啥都不做。 因为我们总是希望中断处理程序尽可能快的返回。但是,中断处理程序注定要完成一部分工作。遗憾的是,并没有谁严格规定说什么任务应该在哪个部分中完成,换 句话说,这个决定完全由想我们这样的驱动工程师来做。记住,中断处理程序会异步执行,并且在最好的情况下它也会锁定当前的中断线,因此将中断处理程序缩短 的越小就越好。当然啦,没有规则并不是没有经验和教训:

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

  我们前边老是说下半部会在以后执行,那么这个以后是个什么概念呢?遗憾的说,这个只是相对于马上而言的。下半部并需要指定一个明确的时间,只要把这个任务 推迟一点,让他们在系统不太繁忙并且中断恢复后执行就可以了。通常下半部在中断处理程序已返回就会马上执行,关键在于当它们运行时,允许相应所有的中断。

  上半部只能通过中断处理程序来完成,下半部的实现却是有很多种方式。这些用来实现下半部的机制分别由不同的接口和子系统组成。最早的是“bottom half”,这种机制也被称为“BH”,它 提供的接口很简单,提供了一个静态创建,由32个bottom half组成的链表,上半部通过一个32位整数中的一位来标识出哪个bottom half可执行。每个BH都在全局范围内进行同步,即使分属于不同的处理器,也不允许任何两个bottom half同时执行。这种方式使用方便但不够灵活,简单却有性能瓶颈。以需要更好的方法了。第二种方法,任务队列(task queues).内核定义了一组队列。其 中每个队列都包含一个由等待调用的函数组成链表。根据其所处队列的位置,这些函数会在某个时刻被执行,驱动程序可根据需要把它们自己的下半部注册到合适的 队列上去。这种方法已经不错,但仍然不够灵活,它没办法代替整个BH接口。对于一些性能要求较高的子系统,像网络部分,它也不能胜任。在2.3开发版中,又引入了软中断(softirqs)和tasklet,这里的软中断和实现系统调用所提到的软中断(软件中断)不是同一个概念。如果无须考虑和过去开发的驱动程序相兼容的话, 软中断和tasklet可以完全代替BH接口。软 中断是一组静态定义的下半部接口,有32个,可以在所有的处理器上同时执行----即使两个类型完全相同。task是一种基于软中断实现的灵活性强,动态 创建的下半部实现机制。两个不同类型的tasklet可以在不同的处理器上同时执行,但类型相同的tasklet不能同时执行。tasklet其实是一种 在性能和易用性之间寻求平衡的产物。软中断必须在编译期间就进行静态注册,而tasklet可以通过代码进行动态注册。现在都是2.6内核了,我们说点新 鲜的吧,linux2.6内核提供了三种不同形式的下半部实现机制:软中断,tasklets和工作对列,这些会依次介绍到。这时,可能有人会想到定时器的概念,定时器也确实是这样,但定时器提供了精确的推迟时间,我们这里还不至于这样,所以先放下,我们后面再说定时器。好,下面我开始说说详细的各种机制:

  1.软中断 实际上软中断使用的并不多,反而是后面的tasklet比较多,但tasklet是通过软中断实现的,软中断的代码位于/kernel /softirq.c中。软中断是在编译期间静态分配的,由softirq_action结构表示,它定义在linux/interrupt.h中:

  每个被注册的软中断都占据该数组的一项,因此最多可能有32个软中断,这是没法动态改变的。由于大部分驱动程序都使用tasklet来实现它们的下半部,所以现在的内核中,只用到了6个。上面的软中断结构中,第一项是软中断处理程序,原型如下:

  当内核运行一个软中断处理程序时,它会执行这个action函数,其唯一的参数为指向相应softirq_action结构体的指针。例如,如果 my_softirq指向softirq_vec数组的实现,那么内核会用如下的方式调用软中断处理程序中的函数:

  一个软中断不会抢占另外一个软中断,实际上,唯一可以抢占软中断的是中断处理程序。不过,其他的软中断----甚至是相同类型的软中断-----可以在其 他类型的机器上同时执行。一个注册的软中断必须在被标记后才能执行----触发软中断(rasing the softirq).通常,中断处理程序会在返回前标记它的软中断,使其在稍后执行。在下列地方,待处理的软中断会被检查和执行:

  软中断会在do_softirq()中执行,如果有待处理的软中断,do_softirq会循环遍历每一个,调用他们的处理程序。核心部分如下:

  上述代码会检查并执行所有待处理的软中断,softirq_pending(),用它来获得待处理的软中断的32位位图-----如果第n位被设置为1, 那么第n位对应类型的软中断等待处理,一旦待处理的软中断位图被保存,就可以将实际的软中断位图清零了。pending &1是判断pending的第一位是否被置为1.一旦pending为0,就表示没有待处理的中断了,因为pending最多可能设置32位,循 环最多也只能执行32位。软中断保留给系统中对时间要求最严格以及最重要的下半部使用。所以使用之前还是要想清楚的。下面简要的说明如何使用软中断:

  索引号越小就越先执行。所以,可以根据自己的需要将自己的索引号放在合适的位置。

  2.注册处理程序:接着,在运行时通过调用open_softirq()注册中断处理程序,例如网络子系统,如下:

  函数有三个参数,软中断索引号,处理函数和data域存放的数组。软中断处理程序执行的时候,允许响应中断,但它自己不能休眠。在一个处理程序运

  行的时候,当前处理器的软中断被禁止,但其他的处理器仍可以执行别的软中断。实际上,如果一个软中断在它被执行的时候同时再次被触发了,那么

  另外一个处理器可以同时运行其处理程序。这意味着对共享数据都需要严格的锁保护。大部分软中断处理程序都通过采取单处理数据(仅属于某一个处

  3.触发软中断:经过上面两项,新的软中断处理程序就能够运行,raist_softirq(中断索引号)函数可以将一个软中断设置为挂起状态,从而让它在下次调

  在中断处理程序中触发软中断是最常见的形式,中断处理程序执行硬件设备的相关操作,然后触发相应的软中断,最后退出。内核在执行完中断处理程序后,马上就会调用do_softirq()函数。于是,软中断就开始执行中断处理程序留给它去完成的剩下任务。

  2.保存中断状态,然后禁止本地中断。在执行tasklet代码时,这么做能够保证处理器上的数据不会弄乱。

  5.如果是多处理器系统,通过检查TASKLET_STATE_RUN来判断这个tasklet是否正在其他处理器上运行。如果它正在运行,那么现在就不要执行,跳

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

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

  8.现在可以确定这个tasklet没有在其他地方执行,并且被我们设置为执行状态,这样它在其他部分就不会被执行,并且引用计数器为0,现在可以执行

  说了这么多,我们该怎样使用这个tasklet呢,这个我在linux设备驱动理论帖讲的太多了。但别急,下边为了博客完整,我仍然会大致讲讲:

  1.声明自己的tasklet:投其所好,既可以静态也可以动态创建,这取决于选择是想有一个对tasklet的直接引用还是间接引用。静态创建方法(直接引用),可以使用下列两个宏的一个(在linux/interrupt.h中定义):

  这两个宏之间的区别在于引用计数器的初始值不同,前面一个把创建的tasklet的引用计数器设置为0,使其处于激活状态,另外一个将其设置为1,处于禁止状态。而动态创建(间接引用)的方式如下:

  2.编写tasklet处理程序:函数类型是void tasklet_handler(unsigned long data).因为是靠软中断实现,所以tasklet不能休眠,也就是说不能在tasklet中使用信号量或者其他什么阻塞式的函数。由于tasklet 运行时允许响应中断,所以必须做好预防工作,如果新加入的tasklet和中断处理程序之间共享了某些数据额的话。两个相同的tasklet绝不能同时执 行,如果新加入的tasklet和其他的tasklet或者软中断共享了数据,就必须要进行适当地锁保护。

  3.调度自己的tasklet:前边的工作一做完,接下来就剩下调度了。通过一下方法实 现:tasklet_schedule(&my_tasklet).在tasklet被调度以后,只要有合适的机会就会得到运行。如果在还没有得 到运行机会之前,如果有一个相同的tasklet又被调度了,那么它仍然只会运行一次。如果这时已经开始运行,那么这个新的tasklet会被重新调度并 再次运行。一种优化策略是一个tasklet总在调度它的处理器上执行。调用tasklet_disable()来禁止某个指定的tasklet,如果该 tasklet当前正在执行,这个函数会等到它执行完毕再返回。调用tasklet_disable_nosync()也是来禁止的,只是不用在返回前等 待tasklet执行完毕,这么做安全性就不咋嘀了(因为没法估计该tasklet是否仍在执行)。tasklet_enable()激活一个 tasklet。可以使用tasklet_kill()函数从挂起的对列中去掉一个tasklet。这个函数会首先等待该tasklet执行完毕,然后再 将其移去。当然,没有什么可以阻止其他地方的代码重新调度该tasklet。由于该函数可能会引起休眠,所以禁止在中断上下文中使用它。

  接下来的问题,我在前边说过,对于软中断,内核会选择几个特殊的实际进行处理(常见的是中断处理程序返回时)。软中断被触发的频率有时会很好,而且还可能 会自行重复触发,这带来的结果就是用户空间的进程无法获得足够的处理器时间,因为处于饥饿状态。同时,如果单纯的对重复触发的软中断采取不立即处理的策略 也是无法接受的。两种极端但完美的情况是什么样的呢:

  2.选择不处理重新触发的软中断。在从中断返回的时候,内核和平常一样,也会检查所有挂起的软中断并处理它们,但是,任何自行重新触发的软中断都

  我现在想的是来个折衷方案吧,那多好,内核开发者门还真是想到了。内核选中的方案是不会立即处理重新触发的软中断,作为改进,当大量软中断出现的时候,内 核会唤醒一组内核线程来处理这些负载。这些线程在最低优先级上运行(nice值为19)。这种这种方案能够保证在软中断负担很重的时候用户程序不会因为得 不到处理时间而处理饥饿状态。相应的,也能保证“过量”的软中断终究会得到处理。最后,在空闲系统上,这个方案同样表现良好,软中断处理得非常迅速(因为 仅存的内存线程肯定会马上调度)。为了保证只要有空闲的处理器,它们就会处理软中断,所以给每个处理器都分配一个这样的线程。所有线程的名字都叫做 ksoftirad/n,区别在于n,它对应的是处理器的编号。一旦该线程被初始化,它就会执行类似下面这样的死循环:

  softirq_pending()负责发现是否有待处理的软中断。当所有需要执行的操作都完成以后,该内核线程将自己设置为 TASK_INTERRUPTIBLE状态,唤起调度程序选择其他可执行进程投入运行。最后,只要do_softirq()函数发现已经执行过的内核线程 重新触发了自己,软中断内核线程就会被唤醒。

  接着上节的来,我们在上节说了软中断和tasklet,那这最后就是工作队列了哦..

  工作队列和前面讨论的其他形式都不相同,它可以把工作推后,交由一个内核线程去执行----该工作总是会在进程上下文执行。这样,通过工作队列执行代码能 占尽进程上下文的所有优势,最重要的就是工作队列允许重新调度甚至是睡眠。相比较前边两个,这个选择起来就很容易了。我说过,前边两个是不允许休眠的,这 个是允许休眠的,这就很明白了是不?这意味着在你需要获得大量内存的时候,在你需要获取信号量时,在你需要执行阻塞式的I/O操作时,它都会非常有用(先 说话, 这个不是我说的,是书上这么说的哦)。

  工作队列子系统是一个用于创建内核线程的接口,通过它创建的进程负责执行由内核其他部分排到队列里的任务。它创建的这些内核线程被称作工作者线程 (worker threads).工作队列可以让你的驱动程序创建一个专门的工作者线程来处理需要推后的工作。不过,工作队列子系统提供了一个缺省的工作者线程来处理这 些工作。因此,工作队列最基本的表现形式就转变成一个把需要推后执行的任务交给特定的通用线程这样一种接口。缺省的工作线程叫做event/n.每个处理 器对应一个线程,这里的n代表了处理器编号。除非一个驱动程序或者子系统必须建立一个属于自己的内核线程,否则最好还是使用缺省线.工作这线程结构用下面的结构表示:

  2.表示工作的数据结构:所有的工作者线程都是用普通的内核线程来实现的,它们都要执行worker_thread()函数。在它初始化完以后,这个函数 执行一个死循环执行一个循环并开始休眠,当有操作被插入到队列的时候,线程就会被唤醒,以便执行这些操作。当没有剩余的时候,它又会继续休眠。工作有 work_struct(linux/workqueue)结构表示:

  分析一下上面的代码。首先线程将自己设置为休眠状态并把自己加入等待队列。如果工作对列是空的,线程调用schedule()函数进入睡眠状态。如果链表 有对象,线程就将自己设为运行态,脱离等待队列。然后,再次调用run_workqueue()执行推后的工作。好了,接下来,问题就纠结在 run_workqueue(),它完成实际推后到此的工作:

  1.首先,实际创建一些需要推后完成的工作,可以在编译时静态地创建该数据结构:

  3.对工作进行调度。前面的准备工作做完以后,下面就可以开始调度了,只需调用schedule_work(&work).这样work马上就会 被调度,一旦其所在的处理器上的工作者线程被唤醒,它就会被执行。当然如果不想快速执行,而是想延迟一段时间执行,按就用 schedule_delay_work(&work,delay);delay是要延迟的时间节拍,后面讲.

  4.刷新操作。插入队列的工作会在工作者线程下一次被唤醒的时候执行。有时,在继续下一步工作之前,你必须保证一些操作已经执行完毕等等。由于这些原因, 内核提供了一个用于刷新指定工作队列的函数:void flush_scheduled_work(void); 这个函数会一直等待,直到队列中所有的对象都被执行后才返回。在等待所有待处理的工作执行的时候,该函数会进入休眠状态,所以只能在进程上下文中使用它。 需要说明的是,该函数并不取消任何延迟执行的工作。取消延迟执行的工作应该调用:int cancel_delayed_work(struct work_struct *work);这个函数可以取消任何与work_struct 相关挂起的工作。

  5.创建新的工作队列。前边说过最好使用缺省线程,可如果你坚持要使用自己创建的线程,咋办?这时你就应该创建一个新的工作队列和与之相应的工作者线程, 方法很简单,用下面的函数:struct workqueue_struct *create_workqueue(const char *name);name是新内核线程的名字。这样就会创建所有的工作者线程(系统中的每个处理器都有一个)并且做好所有开始处理工作之前的准备工作。在创 建之后,就调用下面的函数吧:

  好了,工作队列也说完了,我还是结合前边一篇,把这三个地板不实现的策略比较一下,方便以后选择.

  首先,tasklet是基于软中断实现的,两者相近,工作队列机制与它们完全不同,靠内核线程来实现。软中断提供的序列化的保障最少,这就要求中断处理函 数必须格外小心地采取一些步骤确保共享数据的安全,两个甚至更多相同类别的软中断有可能在不同的处理器上同时执行。如果被考察的代码本身多线索化的工作做 得非常好,它完全使用单处理器变量,那么软中断就是非常好的选择。对于时间要求严格和执行效率很高的应用来说,它执行的也最快。否则选择tasklets 意义更大。tasklet接口简单,而且两种同种类型的tasklet不能同时执行,所以实现起来也会简单一些。如果需要把任务推迟到进程上下文中完成, 那你只能选择工作队列了。如果不需要休眠,那软中断和tasklet可能更合适。另外就是工作队列造成的开销最大,当然这是相对的,针对大部分情况,工作 队列都能提供足够的支持。从方便度上考虑就是:工作队列,tasklets,最后才是软中断。我们在做驱动的时候,关于这三个下半部实现,需要考虑两点: 首先,是不是需要一个可调度的实体来执行需要推后完成的工作(即休眠的需要),如果有,工作队列就是唯一的选择,否则最好用tasklet。性能如果是最 重要的,那还是软中断吧。

  这些函数有可能被嵌套使用----最后被调用的local_bh_enable()最终激活下半部。函数通过preempt_count为每个进程维护一 个计数器。当计数器变为0时,下半部才能够被处理。因为下半部的处理已经被禁止了,所以local_bh_enable()还需要检查所有现存的待处理的 下半部并执行它们。

  好了,这一次讲完了,画了两次,我们在这两次中提到了一些同时发生的问题,这时可能存在数据共享互斥访问的问题,这个就是内核同步方面的事情了,我们后面再慢慢说这个事。

http://magazinski.com/ziduilie/243.html
锟斤拷锟斤拷锟斤拷QQ微锟斤拷锟斤拷锟斤拷锟斤拷锟斤拷锟斤拷微锟斤拷
关于我们|联系我们|版权声明|网站地图|
Copyright © 2002-2019 现金彩票 版权所有