第8章 下半部和推后执行的工作

第8章 下半部和推后执行的工作

下半部

一般来说:
任务对时间非常敏感,放在中断处理程序中执行。
任务和硬件相关,放在中断处理程序中执行。
任务要保证不被其它中断打断,放在中断处理程序中执行。
其它所有任务,考虑放置在下半部执行。

下半部的环境

下半部可以通过多种机制实现。

  1. “下半部”的起源
    最早linux提供“bottom half”用于实现下半部,简称“BH”。接口简单,提供了一个静态创建、由32个bottom havles 组成的链表。上半部通过一个32位整数中的一位来表示哪个bottom half可以执行。每个BH在全局范围内(所有的处理器)进行同步,不允许两个下半部同时执行。使用方便不够灵活,简单但有性能瓶颈。
  2. 任务队列
    任务队列,task queue。
  3. 软中断和tasklet
    软中断是由一组静态定义的下半部接口,有32个,可以在所有的处理器上同时执行(相同类型也可以)。
    tasklet是一种基于软中断实现的灵活性强、动态创建的下半部实现机制(相同类型不可同时执行)。
    软中断使用与对性能要非常高的情况,如网络。其它情况使用tasklet已经足够。
    软中断是要考虑相同的软中断可能同时被执行,在编译期间就进行静态注册。
    tasklet可以通过代码动态注册。
  4. 发展
    BH和任务队列在2.5版本中已被移除,下半部的实现有三种机制,软中断、tasklet、工作队列。

软中断

实际使用比较少,tasklet更常用,不过后者基于前者。
代码位于kernel/softirq.c文件中。

实现

在编译期间静态分配的。
由结构softirq_action结构表示,定义在<linux/interrupt.h>中。

struct softirq_action{
	void (*action)(struct softirq_action *);
};

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

static struct softirq_action softirq_vec[NR_SOFTIRQS];
  1. 软中断处理程序
    原型如下:
void softirq_handler(struct softirq_action *);

一个软中断不会抢占另一个软中断,但是可以被中断处理程序抢占。
2. 执行软中断
注册的软中断在标记后才会执行,触发软中断(raising the softirq)。一般由中断处理程序在返回前标记它的软中断,使其在稍后执行,但执行时间不确定。
在以下情况中可以被唤起:

  • 从一个硬件中断代码返回时
  • 在ksoftirq内核线程中
  • 在那些显式检查和执行待处理的软中断代码中,如网路子系统
    中断的执行在do_softirq()中执行,该函数会尝试循环处理所有待处理的软中断。
/* 核心代码,检查并处理所有待处理的软中断 */
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);
}

使用软中断

软中断保留给系统中对时间要求最严格以及最重要的下半部使用。
目前,只有两个子系统直接使用软中断(网络和SCSI)。内核定时器和tasklet都是建立在软中断上的。
tasklet可以动态生成,对加锁的要求不高,使用方便,性能也不错,是首选项。
对时间要求严格的话,才使用软中断。

  1. 分配索引
    在编译期间,通过<linux/interrupt.h>中定义的一个枚举类型来静态地声明软中断。索引号代表着优先级,小的优先执行。
    习惯上HI_SOFTIRQ通常作为第一项,RCU_SOFTIRQ作为最后一项。
    已有的tasklet类型:
tasklet优先级软中断描述
HI_SOFTIRQ0优先级高的tasklets
TIMER_SOFTIRQ1定时器的下半部
NET_TX_SOFTIRQ2发送网络数据包
NET_RX_SOFTIRQ3接收网络数据包
BLOCK_SOFTIRQ4BLOCK装置
TASKLET_SOFTIRQ5正常优先权的tasklets
SCHED_SOFTIRQ6调度程序
HRTIMER_SOFTIRQ7高分辨率定时器
RCU_SOFTIRQ8RCU锁定
  1. 注册处理程序

在运行时通过调用open_softirq()注册软中断处理程序,使用两个参数:软中断的索引号和处理函数。
如网络子系统

open_softirq(NET_TX_SOFTIRQ,net_tx_action);
open_softirq(NET_RX_SOFTIRQ,net_rx_action);

同一程序可以被多个处理器同时执行,需要严格的所保护。
3. 触发软中断
raise_softirq()将一个软中断设置为挂起状态,在下次调用do_softirq()函数时投入运行。网络子系统可能会调用:
raise_softirq(NET_TX_SOFTIRQ);

tasklet

基于软中断实现,简单,大多数情况使用

实现

tasklet结构体

struct tasklet_struct{
	struct tasklet_struct *next; /* 链表中的下一个tasklet */
	unsigned long state; /* tasklet的状态 */
	atomic_t count; /* 引用计数器 */
	void (*func)(unsigned long); /* tasklet处理函数 */
	unsigned long data; /* 给tasklet处理函数的参数 */
成员描述
functasklet的处理函数,data是它唯一的参数
stateTASKLET_STATE_SECHED表明tasklet已被调度,正准备投入运行;TASKLET_STATE_RUN表明该tasklet正在运行,只有在多处理器上作为优化来使用。还可以取0
count引用计数器
  1. 调度tasklet
    已调度的tasklet(等同于被触发的软中断)存放在两个数据结构:tasklet_vec(普通tasklet),tasklet_hi_vec(高优先级的tasklet)。
    tasklet由tasklet_schedule()和task_hi_schedule()函数进行调度(区别在于一个使用TASKLET_SOFTIRQ,而另一个使用HI_SOFTIRQ),他们接受一个指向tasklet_struct结构的指针作为参数。
    tasklet_schedule()的执行步骤:
检查tasklet的状态,是否为TASKLET_STATE_SCHED,如果是,说明tasklet已被调度,函数立即返回。
调用_tasklet_schedule()。
保存中断状态,然后禁止本地中断。确保处理器上面的数据不会被弄乱。
把tasklet加到每个处理器的一个tasklet_vec或者tasklet_hi_vec链表的表头上去。
唤起TASKLET_SOFTIRQ或HI_SOFTIRQ软中断,这样在下一次调用do_softirq()时就会执行该tasklet。
恢复中断的状态并返回。 

tasklet_action和tasklet_hi_action是tasklet处理的核心。

禁止中断,为当前处理器检索tasklet链表。
当前处理器上该链表置为NULL,达到清空的效果。
允许相应中断。
循环处理每一个链表上每一个待处理的tasklet。
如果是多处理器,需要检查TASKLET_STATE_RUN来判断这个tasklet是否正在其它处理器上运行。如果正在执行,跳过即可。
如果没有执行,设置为TASKLET_STATE_RUN。
检查count值是否为0,确保tasklet没有被禁止。否则执行下一个。
已经确保tasklet没有在其它处理器上执行,而且引用计数为0,执行tasklet的处理程序。
运行完毕,清楚TASKLET_STATE_RUN。
重复执行下一个tasklet,处理完所有待处理的tasklet。

使用TASKLET

  1. 声明
    静态:

#include <linux/interrupt.h>
/* 都可以静态创建task_struct结构 */

/* 该宏引用计数为0,处于激活状态 */
DECLARE_TASKLET(name,func,data)
/* 该宏引用计数为1,处于禁止状态 */
DECLARE_TASKLET_DISABLED(name,func,data)
DECLARE_TASKLET(my_struct,my_tasklet_handler,dev);
// 等价与
struct tasklet_struct my_struct={NULL,0,ATOMIC_INIT(0),my_tasklet_handler,dev);

动态:

tasklet_init(t,tasklet_handler,dev);
  1. 编写tasklet的处理程序
    函数类型:
void tasklet_handler(unsigned long data)

软中断实现,不能睡眠。不能使用信号量或者阻塞函数。而且tasklet允许相应中断,还要做好处理工作。
3. 调度
将task_struct指针传递给task_schedule()函数即可,只要有机会,就会尽快的执行。
作为优化,一个tasklet总在调度它的处理器上执行。
tasklet_disable()用来禁止某个指定的tasklet。
tasklet_disable_nosync()禁止指定tasklet,无需等待返回,不安全。
tasklet_enable()激活tasklet。
tasklet_kill()从挂起的队列中去除一个tasklet。
4. ksoftirqd
每个处理器都有一组辅助处理软中断(和tasklet)的内核线程。
软中断的被触发的频率会很高,处理函数有时还会自行触发。
方案一:本次执行处理完所有触发的软中断,包括重新触发的软中断。在负载高的情况下,用户空间的任务只能等待。只能在负载低时有效。
方案二:不处理重新触发的软中断。时好时坏,软中断等待,没有充分利用闲置的资源。
折中。内核实现的方案是不会处理重新触发软中断。作为优化,在大量软中断出现的时候,内核会唤起一组内核线程来处理这些负载。这些线程在最低的优先级上运行(nice值是19)。
每个处理器都有这个线程,叫ksoftirq/n,n是处理器编号。初始后执行类似下面的死循环:

for(;;){
	if(!softirq_pending(cpu))
		schedule();
	set_current_state(TASK_RUNING);
	while(softirq_pending(cpu)){
		do_softirq();
		if(need_resched())
			schedule();
	}
	set_current_state(TASK_INTERRUPTIBLE);
}

只要do_softirq()函数发现已经执行过的内核线程重新触发了自己,软中断内核线程就会被唤起。

老的BH机制

淘汰,2.5中去除。

工作队列

work queue把工作退后,交给另一个内核线程去执行,总会在内核上下文中,允许重新调度或者睡眠。
可以使用内核线程替换,但还是建议使用工作队列。
在需要获得大量的内存,在需要获取信号量时,在需要执行阻塞式的I/O操作时,使用它!

实现

工作队列子系统是一个用于创建内核线程的接口,通过它创建的进程负责执行由内核其它部分排到队列中的任务。创建的线程称为工作者线程(worker thread)。
缺省的工作者线程叫做events/n,n是处理器的编号。缺省的工作者线程会从多个地方得到被推后的工作。
在需要工作者线程执行大量的处理操作时,如处理器密集型和性能要求严格的任务,可以进行工作者线程的创建,减少缺省线程的压力。

  1. 表示线程的数据结构
    工作者线程用workqueue_struct结构表示:
/*
 *	外部可见的工作队列抽象是由每个
 *	CPU的工作队列组成的数组
 */
 struct workqueue_struct{
 	struct cpu_workqueue_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是核心数据结构:

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;	/* 关联线程 */
}
  1. 表示工作的数据结构
    所有的工作者线程都是由普通的内核线程实现的,执行work_thread()函数。这个函数执行一个死循环并开始休眠,当有操作队列被插入到队列里的时候,线程就会被唤醒。当没有操作时,又会休眠。
    <linux/workqueue.h>里的work_struct结构体表示工作。
struct work_struct{
	atomic_long_t data;
	struct list_head entry;
	work_func_t func;

每一个工作队列都对应一个工作链表,当被唤醒时,会执行工作链表上的所有工作。没有工作时,会休眠。
work_thread()的简化核心流程如下:

/* cwq 是cpu_workqueue_struct */
for(;;){
	prepare_to_wait(&cwq->more_work,&wait,TASK_INTERRUPTIBLE);
	if(list_empty(&cwq->worklist))
		schedule();
	finish_wait(&cwq->more_work,&wait);
	run_workqueue(&cwq);
}

①将自己设置为休眠状态(设置state),加入到等待队列中。
②如果工作链表是空的,调用schedule()函数进入睡眠状态。
③如果工作队列非空,会将自己设置为TASK_RUNNING,脱离等待队列。
④调用run_workqueue()函数执行被推后的工作。

while(!list_empty(&cwq->worklist)){
	struct work_struct *work;
	work_func_t f;
	void *data;

	work=list_entry(cwq->worklist.next,struct work_struct,entry);
	f=work->func;
	list_del_init(cwq->worklist.next);
	work_clear_pending(work);
	f(work);
	

run_workqueue循环遍历链表上每一个待处理的工作,执行每个节点上的func成员函数:
①当链表不为空时,选取下一个对象。
②获取希望执行的函数func和参数data。
③把该节点从链表上解下来,将待处理标志位pending清零。
④调用函数。
⑤重复执行。
3. 总结
每个工作者线程都由一个cpu_workqueue_struct结构体表示,而workqueue_struct结构体则表示给定类型的所有工作者线程。
工作队列workqueue_strut表示一类工作者线程,是多个处理器上该类工作者线程的集合。对于一类工作者线程,每个处理器上都要有且仅有一个该类工作者线程。

使用工作队列

  1. 创建推后的工作
    创建工作结构体
DECLARE_WORK(name,void (*func)(void*),void *data);
INIT_WORK(struct work_struct *work,void(*func)(void*),void *data);
  1. 处理函数编写
void work_handler(void *data)

运行在进程上下文中。默认响应中断,不持有锁。因为内核线程在用户空间没有相关的内存映射,不能访问用户空间。(通常在发生系统调用时,内核代表用户空间的进程执行,才能访问用户空间)。
3. 对工作进行调度
交给缺省的events工作线程:

/* work马上被调度,所在处理器上的工作者线程被唤醒后,就会被执行 */
schedule_work(&work);
/* 延时被调度,delay指时钟节拍 */
schedule_delayed_work(&work,delay);
  1. 刷新操作
    确保一些操作被执行。刷新指定工作队列。
void flush_scheduled_work(void);

函数会一直等待,直到所有对象被执行后才返回。在等待所有待处理的工作执行的时候,该函数会进入睡眠,只能在进程上下文中使用。

/* 取消任何与work_struct相关的挂起工作 */
int cancel_delayed_work(struct *work);
  1. 创建新的工作队列
    缺省的工作队列不能满足需求的话,可以创建一个新的工作队列和与其相应的工作者线程。每个处理器上都会创建一个工作者线程。
    只有在明确需要自己的一套线程提高性能的情况下,再这样做。
/* 创建工作队列 */
struct workqueue_struct *create_workqueue(const char *name);

name是内核线程的命名,如events队列的创建:

struct workqueue_struct *keventd_wq;
keventd_wq=create_workqueue("events");

调度工作队列:

int queue_work(struct workqueue_struct *wq,struct work_struct *work);
int queue_delayed_work(struct workqueue_struct *wq,struct work_struct *work,unsigned long delay);

刷新工作队列 :

flush_workqueue(struct workqueue_struct *wq);

下半部机制的选择

下半部上下文顺序执行保障
软中断中断没有
tasklet中断同类型不能同时执行
工作队列进程没有(和进程上下文一样被调度)

下半部之间加锁

在访问共享数据之前,需要禁止中断并得到所得使用权。不管是中断上下文还是进程上下文。

禁止下半部

常见的做法是,先得到一个锁,然后再禁止下半部的处理。

函数描述
void local_bh_disable()禁止本地处理器的软中断和tasklet的处理
void local_bh_enable()激活本地处理器的软中断和tasklet的处理
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值