Linux内核设计与实现之下半部(七)

7.1 下半部

1)中断处理程序的局限:

A) 它以异步方式执行并且可能打断其他重点代码,甚至是其他中断处理程序,因此中断处理程序应该执行得越快越好

B) 当一个中断处理程序正在执行,在最好情况下,与该中断同级的其他中断会被屏蔽,在最坏情况下,当处理器上所有其他中断都会被屏蔽

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

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

2)中断程序只能作为整个硬件中断处理流程一部分,必须有一个快速、异步、简单的处理程序负责对硬件做出迅速响应并完成那些时间要求严格的操作。

3)中断处理流程中的下半部(bottom
half)

A)任务是执行与中断处理密切相关但中断处理程序本身不执行的工作

B)中断处理程序完成的工作越少越好,所有工作都交给下半部执行

C)中断处理程序必须完成一部分工作,都需要通过操作硬件对中断的到达进行确认,有时还会从硬件拷贝数据。只能靠中断处理程序自己完成。

D)中断处理程序异步执行,在最好情况下也会锁定当前的中断线,因此中断处理程序持续的时间缩短到最小程度非常重要。

4)划分上半部和下半部

A)如果一个任务对时间非常敏感,将其放在中断处理程序中执行

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

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

D)其他所有任务,考虑放置在下半部执行

5)下半部的存在的意义(为什么让工作推后执行)

A)中断处理程序运行的时候当前的中断线在所有处理器上都会被屏蔽

B)如果一个处理程序是SA_INTERRUPT类型,执行的时候会禁止所有本地中断(而且把本地中断线全局地屏蔽掉)

C)缩短中断被屏蔽的时间对系统响应能力和性能都至关重要

D)中断处理程序与其他程序,甚至是其他中断处理程序,异步执行,必须尽力缩短中断处理程序的执行

6)放到什么时候去做

A)下半部不需要指明一个确切时间,只要把这些任务推迟一点,让它们在系统不太繁忙并且中断恢复后执行就可以了。

B)通常下半部在中断处理程序一返回就马上运行。

C)下半部执行的关键在于当运行的时候,运行响应所有的中断。

7.1.2 下半部的环境

  1. 下半部只能通过中断处理程序实现,下半部可以通过多种 机制实现。用来实现下半部的机制分别由不同的接口和子系统组成。

2)下半部机制是如何设计和实现的

3)怎样在自己编写的内核代码中使用它们

4)BH机制

A)接口简单

B)它提供一种静态创建、由32个bottom half组成的链表。

C)上半部通过一个32位整数中的一位来标识出哪个bottom half可以执行。

D)每个BH都在全局范围内进行同步。即使分属于不同的处理器,也不允许任何两个bottom
half同时执行。

E)这种机制不够灵活,简单但有瓶颈

5)任务队列(task
queue)机制

A)实现工作的推后执行,并用来代替BH机制

B)内核定义一组队列,其中每个队列都包含一个由等待调用的函数组成链表,根据其所处队列的位置,这些函数会在某个时刻被执行。

C)驱动程序可以把它们自己的下半部注册到合适的队列上去

D)该机制表现不错,但不够灵活,没法代替整个BH接口,对于一些性能要求较高的子系统,像网路部分,它不能胜任

6)软中断机制和tasklet

A)如果无需考虑和过去开发的驱动程序兼容,软中断和tasklet完全可以替代BH接口

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

C)tasklet是一种基于软中断实现的灵活性强、动态创建的下半部实现机制。两个不同类型的tasklet可以在不同的处理器上同时执行,但类型相同的tasklet不能同时执行。它是一种在性能和易用性之间需求平衡的产物。对于大部分下半部处理,用tasklet就够了。像网路这样对性能要求非常高的情况下才需要使用软中断。

D)使用软中断要特别小心,因为两个相同的软中断有可能同时执行。必须在编译期间就进行静态注册。与此相反,tasklet可以通过代码进行动态注册

7)内核定时器

A)一个可以用于将工作推后执行的机制

B)内核定时器把操作推迟到某个确定的时间段之后执行,当必须保证在一个确定的时间段过去后再运行时,应该使用内核定时器

8)下半部概念总结

A)下半部用于指代中断处理流程中推后执行的那一部分,所有用于实现将工作推后执行的内核机制都被称为下半部机制

B)下半部状态

BH 在2.5中去除

任务队列(task
queue) 在2.5中去除

软中断(softirq) 从2.3开始引入

Tasklet 从2.3开始引入

工作队列(work
queue) 从2.5开始引入

7.2 软中断

1)位于<kernel/softirq.c>

2)实现

A)软中断是在编译期间静态分配的

B)软中断由softirq_action结构体表示,它定义在<linux/interrupt.h>

Struct softirq_action {

            Void

(*action) (struct softirq_action *);

            Void

*data;

};

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

Static struct softirq_action softirq_vec[32];

每个被注册的软中断都占据该数组的一项,最多32个软中断,一个定值,最大值没办法动态改变

D)软中断处理程序

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

Void
sotirq_handler(struct softirq_action *);

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

my_softirq->action(my_softirq);

注:内核把整个结构体都传递给软中断处理程序而不仅仅是传递数据值。这可以保证将来在结构体中加入新的域时,无需对所有的软中断处理程序都进行变动。如果需要,软中断处理程序可以方便的解析它的参数,从数据成员中提取数值。

一个软中断不会抢占另一个软中断。唯一可以抢占软中断的是中断处理程序。其他的软中断甚至是相同类型的软中断可以在其他处理器上同时执行

E)执行软中断

一个注册的软中断必须被标记后才会执行,这被称作触发软中断(raising
the softirq)。

中断处理程序会在返回前标记它的软中断,使其在稍后被执行,然后在适合的时刻,该软中断就会运行。在下列地方,待处理的软中断会被检测和执行:

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

在ksoftirq内核线程中

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

软中断都要在do_softirq()中执行,如果有待处理的软中断,do_softirq()会循环遍历每一个,调用它们的处理程序。

U32 pending = softirq_pending(cpu);

If(pending)

{

            Struct

softirq_action *h = softirq_vec;

            Softirq_pending(cpu)

= 0;

            Do

            {

                            If(pending

& 1)

                                            h->action(h);

                            h++;

                            pending

= 1;

} while(pending);

}

7.2.2 使用软中断

1)软中断

A)软中断保留给系统中对时间要求最严格以及最重要的下半部使用,目前只有两个子系统:网络和SCSI
直接使用软中断

B)内核定时器和tasklet都是建立在软中断上

C)tasklet可以动态生成,由于它们对加锁要求不高,所以使用起来很方便,而且性能也非常不错。对于时间严格要求并能自己高效地完成加锁工作的应用,软中断会是正确选择。

2)分配索引

A)在编译期间,可以通过<linux/interrupt.h>中定义的一个枚举类型来静态地声明软中断

B)内核用这些从0开始的索引来表示相对优先级,索引号小的软中断在索引号大的软中断之前执行

C)tasklet类型列表

HI_SOFTIRQ 0 优先级高的tasklet

TIMER_SOFTIRQ 1 定时器的下半部

NET_TX_SOFTIRQ 2 发送网络数据包

NET_RX_SOFTIRQ 3 接收网络数据包

SCSI_SOFTIRQ 4 SCSI的下半部

TASKLET_SOFTIRQ 5 tasklet

D)建立一个新的软中断必须在此枚举类型中加入新的项,不能简单地把新项加到列表的末尾,相反必须根据希望赋予它的优先级来决定加入的位置

3)注册处理程序

Open_softirq(NET_TX_SOFTIRQ, net_tx_action, NULL);

Open_softirq(NET_TX_SOFTIRQ, net_tx_action, NULL);

软中断处理程序执行的时候,允许响应中断,但它自己不能休眠。

在一个处理程序运行的时候,当前处理器上的软中断被禁止,但其他处理器扔可以执行别的软中断。

如果一个软中断在它被执行的同时再次被触发了,那么另一个处理器可以同时运行其处理程序,这意味着任何共享数据,甚至是仅在软中断处理程序内部使用的全局变量,都需要严格的所保护。

如果仅仅通过互斥加锁方式来防止它自身的并发执行,那么使用软中断没有任何意义。

大部分软中断处理程序都通过采取单处理数据(仅属于某一个处理器的数据,因此根本不需要加锁)或其他一些技巧来避免显示地加锁,从而提供更出色的性能。

引入软中断的主要原因是其可扩展性。如果不需要扩展到多个处理器,那么就使用tasklet。

4)触发软中断

A)通过在枚举类型的列表中添加新项已经调用open_softirq()进行注册后,新的软中断处理程序就能够运行。

B)raise_softirq()函数可以将一个软中断设置为挂起状态,让它在下次调用do_softirq()函数时投入运行。

网络子系统可能会调用:raise_softirq(NET_TX_SOFTIRQ);这会触发NET_TX_SOFTIRQ软中断。它的处理程序net_tx_action()就会在内核下一次执行软中断时投入运行。该函数在触发软中断之前先要禁止中断,触发后再恢复原来的状态。如果中断已经被禁止了,那么可以调用另一个函数raise_softirq_irqoff(),这会带来一些优化结果。

Raise_softirq_irqoff(NET_TX_SOFTIRQ);

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

7.3 tasklet

1)它是利用软中断实现的一种下半部机制,它和进程没有任何关系。

2)它的接口更简单,锁保护要求较低。

3)通常选择tasklet,软中断使用较少。

7.3.1 tasklet的实现

1)它由两类软中断代表:HI_SOFTIRQ和TASKLET_SOFTIRQ,这两者唯一的区别在于HI_SOFTIRQ类型的软中断先于TASKLET_SOFTIRQ类型的软中断执行

2)tasklet结构体

A)Tasklet由tasklet_struct结构体表示,每一个结构体单独代表一个tasklet,它在<linux/interrupt.h>定义。

B)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是不是正在运行。

Count成员是tasklet的引用计数器。如果它不为0,则tasklet被禁止,不允许执行;只有当它为0时,tasklet才会被激活,并且设置为挂起状态时,该tasklet才能执行。

3)调度tasklet

A)已调度的tasklet(等同于被触发的软中断)存放在两个单处理器数据结构:tasklet_vec(普通tasklet)和tasklet_hi_vec(高优先级的tasklet)中。这两个数据结构都是由tasklet_struct结构体构成的链表。链表中的每个tasklet_struct代表一个不同的tasklet。

B)tasklet由tasklet_schedule()和tasklet_hi_schedule()函数进行调度,它们接受一个指向tasklet_struct结构的指针作为参数(两个函数的区别是一个使用TASKLET_SOFTIRQ,另一个使用HI_SOFTIRQ)

C)tasklet_schedule()的细节

检查tasklet的状态是否为TASKLET_STATE_SCHED。是,则tasklet已经被调度过,函数立即返回;保存中断状态,然后禁止本地中断(在执行tasklet代码时,能保证当tasklet_schedule()处理这些tasklet时,处理器上的数据不会弄乱);把需要调度的tasklet加到每一个处理器的tasklet_vec链表或tasklet_hi_vec链表的表头上去;唤起TASKLET_SOFTIRQ或HI_SOFTIRQ软中断,这样下一次调用do_softirq()时就会执行tasklet;恢复中断到原来状态并返回。

D)do_softirq()会尽可能早地在下一个适合的时间执行,由于大部分tasklet和软中断都是在中断处理程序中被设置成待处理状态,所以最近一个中断返回的时候看起来就是执行do_softirq()的最佳时机。因为TASKLET_SOFTIRQ和HI_SOFTIRQ已经被触发,所以do_softirq()会执行相应的软中断处理程序,而这两个处理程序,tasklet_action()和tasklet_hi_action(),就是tasklet处理的核心。

E)软中断处理函数具体做了什么

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

2.将当前处理器上的链表设置为NULL(达到清空的效果);

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

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

5.如果是多处理器系统,通过检查TASKLET_STATE_RUN状态标志来判断这个tasklet是否正在其他处理器上运行,如果正在运行,现在就不要执行,跳到下一个待处理的tasklet去(同一个时间里,相同类型的tasklet只能有一个执行);如果当前这个tasklet没有执行,将其状态设置为TASKLET_STATE_RUN(这样其他处理器就不会再去执行它);

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

7.tasklet没有在其他地方执行且被设置成执行状态,同时引用计数为0,现在可以执行tasklet的处理程序;

8.tasklet运行完毕,清楚tasklet的state域的TASKLET_STATE_RUN状态标志;

9.重复执行下一个tasklet,直到没有剩余的等待处理的tasklet;

F)tasklet的实现很简单,所有的tasklet都是通过重复运用HI_SOFTIRQ和TASKLET_SOFTIRQ这两个中断来实现。当一个tasklet被调度时,内核会唤起这两个软中断中的一个,随后,该软中断会被特定的函数处理,执行所有已被调度的tasklet。这个函数保证同一时间里只有一个给定类别的tasklet会被执行(其他不同类型的tasklet可以同时执行)。

7.3.2 使用tasklet

1)声明tasklet,可以静态创建,也可以动态创建,选择哪种方式取决于到底是有一个队tasklet的直接引用还是间接引用。

A)静态创建一个tasklet(直接引用),使用下面<linux/interrupt.h>中定义的两个宏中的一个:

DECLAR_TASKLET(name,func,data)

DECLAR_TASKLET_DISABLED(name,func,data)

当该tasklet被调度以后,给定的函数func会被执行,它的参数由data给出。

这两个宏的确保在于引用计数器的初始值设置不同,前一个设置把创建的tasklet的引用计数器设置为0,该tasklet处于激活状态,后一个设置为1,tasklet处于禁止状态。

DECLARE_TASKLET(my_tasklet,my_tasklet_handler,dev)

《==》

Struct tasklet_struct my_tasklet = {

            NULL,0, ATOMIC_INT(0),

            My_tasklet_handler,dev

};

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

B)动态创建tasklet(间接引用)

tasklet_init(t,tasklet_handler,dev);

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

2)编写自己的tasklet处理程序

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

Void tasklet_handler(unsigned
long data);

因为是靠软中断实现,所以tasklet不能睡眠,这意味着你不能在tasklet中使用信号量或者其他阻塞式的函数。由于tasklet运行时允许响应中断,所以必须做好预防工作(屏蔽中断然后获取一个锁),如果你的tasklet和中断处理程序直接共享了某些数据。两个相同的tasklet绝不会同时执行,这点和软中断不同,尽快两个不同的tasklet可以在两个处理器上同时执行。如果你的tasklet和其他的tasklet或者软中断共享了数据,必须进行适当地锁保护。

3)调用自己的tasklet

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

Tasklet_schedule(&my_tasklet);

在tasklet被调度以后,只要有机会它就会尽可能早地运行。在没有机会运行之前,如果有一个相同的tasklet又被调度了(这里应该是唤起),那么它仍然只会运行一次。如果这时它已经开始运行,比如说在另一个处理器上,那么这个新的tasklet会被重新调度并再次运行。作为一种优化措施,一个tasklet总是在调度它的处理器上执行,这是希望更好地利用处理器的高速缓存。

B)调用tasklet_disable()函数来禁止某个指定的tasklet。

如果该tasklet正在执行,这函数会等到它执行完毕再返回。也可以调用tasklet_disable_nosync()函数,它可用来禁止指定的tasklet,且无须在返回前等待tasklet执行完毕(不太安全,无法估计该tasklet是否仍在执行)

Tasklet_disable(&my_tasklet);

C)调用tasklet_enable()函数可以激活一个tasklet。

如果希望激活DECLARE_TASKLET_DISABLED()创建的tasklet,也可以调用该函数。

Tasklet_enable(&my_tasklet);

D)通过tasklet_kill()函数从挂起的队列中去掉一个tasklet。

该函数的参数是一个指向某个tasklet的tasklet_struct的长指针。

在处理一个经常重新调度它自身的tasklet的时候,从挂起的队列中移除已调用的tasklet很有用。

这个函数首先等待该tasklet指向完毕,然后再将它移除。

该函数可能会引起休眠,所以禁止在中断上下文中使用它。

7.3.3 ksoftirq

7.3.4 老的BH机制

7.4 工作队列

7.4.1 工作队列的实现

7.4.2 使用工作队列

7.4.3 老的任务队列机制

7.5 下半部机制的选择

1)三种可能的选择:软中断、tasklet和工作队列

2)设计的角度考虑,软中断提供的执行序列化的保障最少,这要求软中断处理函数必须格外小心地采用一些步骤确保共享数据的安全,两个甚至更多相同类别的软中断有可能在不同的处理器上同时执行。

A)如果被考察的代码本身多线索化的工作就做得非常好,比如像网路子系统,它就完全使用单处理器变量,那么软中断就是非常好的选择。对于时间要求严格和执行频率很高的应用来说,它执行得也最快。

B)如果代码多线索化考虑得并不充分,那么选择tasklet意义更大。其接口简单,同种类型的tasklet不能同时执行。驱动程序开发者应该竟可能选择tasklet,而不是软中断。

C)如果准备利用每一个处理器上的变量或者类似的情形,以确保软中断能够安全地在多个处理器上并发地运行,那么还是选择软中断。

D)如果需要把任务推后到进程上下文中完成,那么只能选择工作队列。

E)如果进程上下文并不是必须得条件,就是如果并不需要睡眠,那么软中断和tasklet可能更适合。

F)工作队列造成的开销最大,因为它要牵扯到内核线程甚至是上下文切换。并不是说工作队列的效率低,如果每秒有几千次中断,就像网路子系统时常经历的那样,那么采用其他机制可能更适合一些。尽快如此,针对大部分情况,工作队列都能够提供足够的支持。

G)工作队列最易于使用,接下来是tasklet,它的接口简单;最好是软中断,它必须静态创建,并且需要慎重考虑其实现

H)三种下半部接口对比

下半部 上下文 顺序执行保障

软中断 中断 没有

Tasklet 中断 同类型不能同时执行

工作队列 进程 没有(和进程上下文一样被调度)

I)驱动程序的编写者需要做的两个选择

1.是否需要一个可调度的实体来执行需要推后的工作,从根本上说,有休眠需要吗?

2.要是有,工作队列就是唯一的选择;否则最好用tasklet。要是专注于性能的提高,那就得考虑软中断

7.6 在下半部之间加锁

1)使用下半部机制时,即使在一个单处理器的系统上,避免共享数据被同时访问页是至关重要的。下半部可能在任何时候执行。

2)使用tasklet的一个好处在于它自己负责执行的序列化保障:两个相同类型的tasklet不允许同时执行,即使在不同的处理器上也不行。Tasklet之间的同步(当两个不同类型的tasklet共享同一个数据时)需要正确使用锁机制。

3)软中断根本不保障执行序列化(即使相同类型的软中断也有可能有两个实例在同时执行),所以所有的共享数据都需要合适的锁。

4)如果进程上下文和一个下半部共享数据,在访问这些数据之前,需要禁止下半部的处理并得到锁的使用权(所做的这些是为了本地和SMP的保护并且防止死锁的出现)

5)如果中断上下文和一个下半部共享数据,在访问数据之前,需要禁止中断并得到锁的使用权(所做的这些是为了本地和SMP的保护并且防止死锁的出现)

6)任何在工作队列中被共享的数据也需要使用锁机制。其中有关锁的要点和在一般内核代码中没有什么区别,因为工作队列本来就是在进程上下文中执行的。

7)禁止下半部

A)单纯禁止下半部的处理是不够的,为了保证共享数据的安全,更常见的做法是先得到一个锁然后再禁止下半部的处理。驱动程序中通常使用这种方法。内核的核心代码,可能只需要禁止下半部就可以了。

B)禁止所有下半部(确切地说,是所有的软中断和所有的tasklet),可以调用local_bh_disable()函数。运行下半部进行处理,可以调用local_bh_enable()函数。

C)下半部机制控制函数的清单

7.7 下半部处理小结

1)延迟linux内核工作的三种机制:软中断、tasklet和工作队列

2)机制设计和实现

3)如何把这些机制应用到代码中

4)历史的下半部机制:BH和任务队列

5)下半部需要用到同步和并发

6)禁止下半部的问题,这是因为并发保护引起的

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值