下半部

i.  下半部

1.  下半部

执行与中断处理密切相关但中断处理程序本身不执行的工作。

2. 为什么用下半部

中断处理流程(上半部)的局限性

a) 中断处理程序以异步方式执行并且可能会打断重要代码(也有可能是其他中断处理程序)的执行。为了避免被打断的代码停止时间过长,中断处理程序执行得越快越好。

b) 如果当前有一个中断处理程序正在执行,在最好的情况下,该中断同级的其他中断会被屏蔽,在最坏的情况下,当前处理器上所有其他中断都会被屏蔽。因此,让其执行得越快越好。

c) 由于中断处理程序要对硬件操作,它们通常有很高的时限要求。

d) 中断处理程序不能运行在进程上下文,不能阻塞。限制了其所作的事情。

中断处理流程(上半部)负责对硬件迅速响应并完成时间要求严格的操作。对时间要求宽松的任务,推迟到中断被激活以后再去运行。

下半部的优势:推迟运行强调的是不是马上运行,不需要指明一个确切的时间,只要把这些任务推迟一点,让它们在系统不太繁忙并且中断恢复后执行。通常下半部在中断处理程序一返回马上运行。下半部执行的关键在于当它们运行的时候会,允许响应所有的中断。

3. 上半部,下半部任务划分

对于上半部和下半部之间划分工作,尽管不存在某种严格的规则,但还是有一些提示:

a) 一个任务对时间非常敏感,将其放在上半部。

b)  一个任务和硬件相关,将其放在上半部。

c) 一个任务保证不被其他中断(特别是相同的中断)打断,将其放在上半部。

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

4. 下半部实现机制

下半部实现:软中断,tasklet,工作队列。

 

Softirq

Tasklet

Work Queue

执行上下文

延后的工作,运行于中断上下文

延后的工作,运行于中断上下文

延后的工作,运行于进程上下文

可重用

可以在不同的CPU上同时运行

不能在不同的CPU上同时运行,

但是不同的CPU可以运行不同的tasklet

可以在不同的CPU上同时运行

睡眠

不能睡眠

不能睡眠

可以睡眠

抢占

不能抢占/调度

不能抢占/调度

可以抢占/调度

易用性

不容易使用

容易使用

容易使用

何时使用

如果延后的工作不会睡眠,

而且有严格的可扩展性或速度要求

如果延后的工作不会睡眠

如果延后的工作会睡眠

 

ii. 软中断

软中断保留给系统中对时间要求最严格以及最重要的下半部使用。网络和SCSI直接使用软中断。内核定时器和tasklet都是建立在软中断上面。下面以网络子系统为例说明如何使用软中断。

1.  软中断处理程序

static void net_tx_action(struct softirq_action *h);

static void net_rx_action(struct softirq_action *h)

2.  注册软中断处理程序

open_softirq(NET_TX_SOFTIRQ, net_tx_action);

open_softirq(NET_RX_SOFTIRQ, net_rx_action);

3. 触发软中断

raise_softirq(NET_TX_SOFTIRQ);

raise_softirq(NET_RX_SOFTIRQ);

raise_softirq()函数将一个软中断设置为挂起状态,让它在下次调用do_softirq()函数时运行处理程序。

4. 执行软中断

在合适的时刻,软中断会运行。在下列地方,待处理的软中断会被检查和执行:

a)      从一个硬件中断代码返回时。

b)      在ksoftirqd内核线程中。

c)      显式检查和执行待处理的软中断代码中,如网络子系统。

软中断在do_softirq()中执行。如果有待处理的软中断,此函数会循环遍历每一个,调用它们的处理函数。核心代码如下:


h->action(h)执行实际的软中断处理函数

iii. Tasklet

大多数情况下,为了控制一个寻常的硬件设备,tasklet机制都是实现下半部的最佳选择。Tasklet可以动态创建,使用方便,执行起来还算快。

1.  Tasklet的实现

a) 注册tasklet

在softirq_init()函数中调用open_softirq(TASKLET_SOFTIRQ, tasklet_action);和open_softirq(HI_SOFTIRQ,tasklet_hi_action);函数注册tasklet处理函数。

b) 触发tasklet action函数

Tasklet_schedule()和task_hi_schedule函数进行调度tasklet。下面为两个函数实现的简单说明:

1)  检查tasklet的状态是否为TASK_STATE_SCHED.如果是,说明tasklet已经被调度过。函数立即返回。

2) 保存中断状态, 然后禁止本地中断。

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

4) 唤起TASKLET_SOFTIRQ 或HI_SOFTIRQ软中断,这样在下一次调用do_softirq()函数时执行此tasklet

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

c) asklet_action()和tasklet_hi_action()软中断处理函数

这两个函数是tasklet处理的核心。下面是简单实现:

1)      禁止中断,并检索保存当前处理器tasklet_vec和tasklet_hi_vec链表。

2)      清空当前处理器的链表。

3)      允许响应中断。

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

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

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

7)      检查count是否为0,确保tasklet没有被禁止。如果被禁止,跳到下一个tasklet。

8)      执行tasklet的处理程序。

9)      Tasklet执行完毕,清除tasklet的state域的TASKLET_STATE_RUN状态标志。

10)  重复执行下一个tasklet,直到没有剩余的等待处理的tasklet。

核心代码如下:


2.      使用tasklet

a) asklet处理程序

void short_do_tasklet(unsigned long);

b) 声明tasklet

静态创建:DECLARE_TASKLET(short_tasklet, short_do_tasklet, 0);

动态创建:tasklet_init(t, short_do_tasklet, dev);

c) 调度tasklet

tasklet_schedule(&short_tasklet);

d) 禁用或激活tasklet

Tasklet_disable(&short_tasklet);

如果该tasklet正在运行,这个函数会等到它执行完毕再返回。

Tasklet_disable_nosync(&short_tasklet);

无须在返回前等待tasklet执行完毕。

Tasklet_enable(&short_tasklet);

e) 去掉tasklet

从挂起的队列中去掉一个tasklet。这个函数等待该tasklet执行完毕,然后将它移去。该函数可能会引起休眠,禁止在中断上下文中使用。

iv. Ksoftirqd

Problem:如果软中断出现的频率很高,再加上它们又有将自己重新设置为可执行状态的能力,就会导致用户空间进程无法获得足够的处理器时间,处于饥饿状态。如果单纯的对重新触发的软中断采取不立即处理的策略,也无法接受。

Solution: 1. 只要还有被触发并等待处理的软中断,本次执行就负责处理,重新触发的软中断也在本次执行返回前处理。这样可以保证即时处理。但系统可能一直处理软中断,不能完成用户任务。这样,用户空间根本不能容忍有明显的停顿出现。2. 选择不处理重新触发的软中断。从中断返回的时候,检查挂起的软中断并处理,但重新触发的软中断不马上处理,等一段时间,到下一个软中断执行时处理。但比较空闲的系统中,立即处理软中断才是比较好的做法。

结论:设计软中断时,做一个折中。当大量软中断出现的时候,内核会唤醒一组内核线程来处理这些负载。这些线程在最低的优先级上运行,这能避免和其他重要的任务抢夺资源。

每个处理器都有一个这样的线程。此线程的名字叫ksoftirqd/n。一旦该线程被初始化,它就会执行类似下面的死循环:


v. 工作队列

工作队列是另外一种将工作推后执行的形式。它把工作推后,交给内核线程去执行—运行在进程上下文。由于执行于进程上下文,它允许重新调度或睡眠。

1. 工作队列的实现

a) 数据结构间的关系

  

b) worker_thread()函数

    初始化结束后,这个函数执行一个死循环并开始休眠。当有操作被插入到队列后,线程被唤醒,以便执行下半部操作。此函数核心流程如下:

  

c) run_workqueue()函数

   

2.  使用工作队列

a) 静态创建: DECLARE_WORK(name, void(*func)(void *)data);

   动态创建:INIT_WORK(struct work_struct *work,void(*func)(void*), void *data);

b) 工作队列处理函数

   Void work_handler(void *data);

   该函数运行在进程上下文中,允许响应中断,不持有任何锁。

c) 对工作队列进行调度

   Schedule_work(&work);

   Schedule_delayed_work(&work, delay);

d) 刷新操

    排入队列的工作会在工作者线程下一次被唤醒的时候执行。但在继续下一步工作之前,有可能需要保证一些操作已经执行完毕(卸载)。基于以上 考虑,内核提供了用于刷新指定工作队列的函数。

  Void flush_scheduled_work(void);

e) 创建新的队列

   Struct workqueue_struct *create_workqueue(const char * name);

   Queue_work(&wq, &work) à Schedule_work(&work);

   Queue_delayed_work(&wq,&work,delay) àSchedule_delayed_work(&work,delay);

   Flush_workqueue(&wq) -> flush_scheduled_work(void);

 

vi. 在下半部加锁

1.  asklet之间同步:加锁

2. 软中断之间共享数据:加锁

3. 进程上下文和下半部共享数据:加锁并禁止下半部的处理

   本地和SMP的保护并且防止死锁

   禁止下半部:void local_bh_diable()

  

   激活下半部:void local_bh_enable()

  

4. 中断上下文和下半部共享数据:加锁并禁止中断。

   本地和SMP的保护并且防止死锁

 


  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值