源地址:http://www.ibm.com/developerworks/cn/linux/kernel/interrupt/

Linux 2.4.x内核软中断机制

杨沙洲 (pubb@163.net)国防科技大学计算机学院
杨沙洲,现为国防科技大学计算机学院博士生,主要研究领域为操作系统技术。

简介: 本文从Linux内核几种软中断机制相互关系和发展沿革入手,分析了这些机制的实现方法,给出了它们的基本用法。

发布日期: 2002 年 6 月 01 日
级别: 初级
访问情况 : 5213 次浏览
评论:  (查看 | 添加评论 - 登录)

平均分 (10个评分)
为本文评分

软中断概况

软中断是利用硬件中断的概念,用软件方式进行模拟,实现宏观上的异步执行效果。很多情况下,软中断和"信号"有些类似,同时,软中断又是和硬中断相对应的,"硬中断是外部设备对CPU的中断","软中断通常是硬中断服务程序对内核的中断","信号则是由内核(或其他进程)对某个进程的中断"(《Linux内核源代码情景分析》第三章)。软中断的一种典型应用就是所谓的"下半部"(bottom half),它的得名来自于将硬件中断处理分离成"上半部"和"下半部"两个阶段的机制:上半部在屏蔽中断的上下文中运行,用于完成关键性的处理动作;而下半部则相对来说并不是非常紧急的,通常还是比较耗时的,因此由系统自行安排运行时机,不在中断服务上下文中执行。bottom half的应用也是激励内核发展出目前的软中断机制的原因,因此,我们先从bottom half的实现开始。


回页首

bottom half

在Linux内核中,bottom half通常用"bh"表示,最初用于在特权级较低的上下文中完成中断服务的非关键耗时动作,现在也用于一切可在低优先级的上下文中执行的异步动作。最早的bottom half实现是借用中断向量表的方式,在目前的2.4.x内核中仍然可以看到:

static void (*bh_base[32])(void);	         /* kernel/softirq.c */

系统如此定义了一个函数指针数组,共有32个函数指针,采用数组索引来访问,与此相对应的是一套函数:

void init_bh(int nr,void (*routine)(void));

为第nr个函数指针赋值为routine。

void remove_bh(int nr);

动作与init_bh()相反,卸下nr函数指针。

void mark_bh(int nr);

标志第nr个bottom half可执行了。

由于历史的原因,bh_base各个函数指针位置大多有了预定义的意义,在v2.4.2内核里有这样一个枚举:

enum {
TIMER_BH = 0,
TQUEUE_BH,
DIGI_BH,
SERIAL_BH,
RISCOM8_BH,
SPECIALIX_BH,
AURORA_BH,
ESP_BH,
SCSI_BH,
IMMEDIATE_BH,
CYCLADES_BH,
CM206_BH,
JS_BH,
MACSERIAL_BH,
ISICOM_BH
};

并约定某个驱动使用某个bottom half位置,比如串口中断就约定使用SERIAL_BH,现在我们用得多的主要是TIMER_BH、TQUEUE_BH和IMMEDIATE_BH,但语义已经很不一样了,因为整个bottom half的使用方式已经很不一样了,这三个函数仅仅是在接口上保持了向下兼容,在实现上一直都在随着内核的软中断机制在变。现在,在2.4.x内核里,它用的是tasklet机制。


回页首

task queue

在介绍tasklet之前,有必要先看看出现得更早一些的task queue机制。显而易见,原始的bottom half机制有几个很大的局限,最重要的一个就是个数限制在32个以内,随着系统硬件越来越多,软中断的应用范围越来越大,这个数目显然是不够用的,而且,每个bottom half上只能挂接一个函数,也是不够用的。因此,在2.0.x内核里,已经在用task queue(任务队列)的办法对其进行了扩充,这里使用的是2.4.2中的实现。

task queue是在系统队列数据结构的基础上建成的,以下即为task queue的数据结构,定义在include/linux/tqueue.h中:

struct tq_struct {
struct list_head list; /* 链表结构 */
unsigned long sync; /* 初识为0,入队时原子的置1,以避免重复入队 */
void (*routine)(void *); /* 激活时调用的函数 */
void *data; /* routine(data) */
};
typedef struct list_head task_queue;

在使用时,按照下列步骤进行:

  1. DECLARE_TASK_QUEUE(my_tqueue); /* 定义一个my_tqueue,实际上就是一个以tq_struct为元素的list_head队列 */
  2. 说明并定义一个tq_struct变量my_task;
  3. queue_task(&my_task,&my_tqueue); /* 将my_task注册到my_tqueue中 */
  4. run_task_queue(&my_tqueue); /* 在适当的时候手工启动my_tqueue */

大多数情况下,都没有必要调用DECLARE_TASK_QUEUE()定义自己的task queue,因为系统已经预定义了三个task queue:

  1. tq_timer,由时钟中断服务程序启动;
  2. tq_immediate,在中断返回前以及schedule()函数中启动;
  3. tq_disk,内存管理模块内部使用。

一般使用tq_immediate就可以完成大多数异步任务了。

run_task_queue(task_queue *list)函数可用于启动list中挂接的所有task,可以手动调用,也可以挂接在上面提到的bottom half向量表中启动。以run_task_queue()作为bh_base[nr]的函数指针,实际上就是扩充了每个bottom half的函数句柄数,而对于系统预定义的tq_timer和tq_immediate的确是分别挂接在TQUEUE_BH和IMMEDIATE_BH上(注意,TIMER_BH没有如此使用,但TQUEUE_BH也是在do_timer()中启动的),从而可以用于扩充bottom half的个数。此时,不需要手工调用run_task_queue()(这原本就不合适),而只需调用mark_bh(IMMEDIATE_BH),让bottom half机制在合适的时候调度它。


回页首

tasklet

由上看出,task queue以bottom half为基础;而bottom half在v2.4.x中则以新引入的tasklet为实现基础。

之所以引入tasklet,最主要的考虑是为了更好的支持SMP,提高SMP多个CPU的利用率:不同的tasklet可以同时运行于不同的CPU上。在它的源码注释中还说明了几点特性,归结为一点,就是:同一个tasklet只会在一个CPU上运行。

struct tasklet_struct
{
struct tasklet_struct *next; /* 队列指针 */
unsigned long state; /* tasklet的状态,按位操作,目前定义了两个位的含义:
TASKLET_STATE_SCHED(第0位)或TASKLET_STATE_RUN(第1位) */
atomic_t count; /* 引用计数,通常用1表示disabled */
void (*func)(unsigned long); /* 函数指针 */
unsigned long data; /* func(data) */
};

把上面的结构与tq_struct比较,可以看出,tasklet扩充了一点功能,主要是state属性,用于CPU间的同步。

tasklet的使用相当简单:

  1. 定义一个处理函数void my_tasklet_func(unsigned long);
  2. DECLARE_TASKLET(my_tasklet,my_tasklet_func,data); /* 定义一个tasklet结构my_tasklet,与my_tasklet_func(data)函数相关联,相当于DECLARE_TASK_QUEUE() */
  3. tasklet_schedule(&my_tasklet); /* 登记my_tasklet,允许系统在适当的时候进行调度运行,相当于queue_task(&my_task,&tq_immediate)和mark_bh(IMMEDIATE_BH) */

可见tasklet的使用比task queue更简单,而且,tasklet还能更好的支持SMP结构,因此,在新的2.4.x内核中,tasklet是建议的异步任务执行机制。除了以上提到的使用步骤外,tasklet机制还提供了另外一些调用接口:

DECLARE_TASKLET_DISABLED(name,function,data); /* 和DECLARE_TASKLET()类似,不过即使被调度到也不会马上运行,必须等到enable */
tasklet_enable(struct tasklet_struct *); /* tasklet使能 */
tasklet_disble(struct tasklet_struct *); /* 禁用tasklet,只要tasklet还没运行,则会推迟到它被enable */
tasklet_init(struct tasklet_struct *,void (*func)(unsigned long),unsigned long); /* 类似DECLARE_TASKLET() */
tasklet_kill(struct tasklet_struct *); /* 清除指定tasklet的可调度位,即不允许调度该tasklet,但不做tasklet本身的清除 */

前面提到过,在2.4.x内核中,bottom half是利用tasklet机制实现的,它表现在所有的bottom half动作都以一类tasklet的形式运行,这类tasklet与我们一般使用的tasklet不同。

在2.4.x中,系统定义了两个tasklet队列的向量表,每个向量对应一个CPU(向量表大小为系统能支持的CPU最大个数,SMP方式下目前2.4.2为32)组织成一个tasklet链表:

struct tasklet_head tasklet_vec[NR_CPUS] __cacheline_aligned;
struct tasklet_head tasklet_hi_vec[NR_CPUS] __cacheline_aligned;

另外,对于32个bottom half,系统也定义了对应的32个tasklet结构:

struct tasklet_struct bh_task_vec[32];

在软中断子系统初始化时,这组tasklet的动作被初始化为bh_action(nr),而bh_action(nr)就会去调用bh_base[nr]的函数指针,从而与bottom half的语义挂钩。mark_bh(nr)被实现为调用tasklet_hi_schedule(bh_tasklet_vec+nr),在这个函数中,bh_tasklet_vec[nr]将被挂接在tasklet_hi_vec[cpu]链上(其中cpu为当前cpu编号,也就是说哪个cpu提出了bottom half的请求,则在哪个cpu上执行该请求),然后激发HI_SOFTIRQ软中断信号,从而在HI_SOFTIRQ的中断响应中启动运行。

tasklet_schedule(&my_tasklet)将把my_tasklet挂接到tasklet_vec[cpu]上,激发TASKLET_SOFTIRQ,在TASKLET_SOFTIRQ的中断响应中执行。HI_SOFTIRQ和TASKLET_SOFTIRQ是softirq子系统中的术语,下一节将对它做介绍。


回页首

softirq

从前面的讨论可以看出,task queue基于bottom half,bottom half基于tasklet,而tasklet则基于softirq。

可以这么说,softirq沿用的是最早的bottom half思想,但在这个"bottom half"机制之上,已经实现了一个更加庞大和复杂的软中断子系统。

struct softirq_action
{
void (*action)(struct softirq_action *);
void *data;
};
static struct softirq_action softirq_vec[32] __cacheline_aligned;

这个softirq_vec[]仅比bh_base[]增加了action()函数的参数,在执行上,softirq比bottom half的限制更少。

和bottom half类似,系统也预定义了几个softirq_vec[]结构的用途,通过以下枚举表示:

enum
{
HI_SOFTIRQ=0,
NET_TX_SOFTIRQ,
NET_RX_SOFTIRQ,
TASKLET_SOFTIRQ
};

HI_SOFTIRQ被用于实现bottom half,TASKLET_SOFTIRQ用于公共的tasklet使用,NET_TX_SOFTIRQ和NET_RX_SOFTIRQ用于网络子系统的报文收发。在软中断子系统初始化(softirq_init())时,调用了open_softirq()对HI_SOFTIRQ和TASKLET_SOFTIRQ做了初始化:

void open_softirq(int nr, void (*action)(struct softirq_action*), void *data)

open_softirq()会填充softirq_vec[nr],将action和data设为传入的参数。TASKLET_SOFTIRQ填充为tasklet_action(NULL),HI_SOFTIRQ填充为tasklet_hi_action(NULL),在do_softirq()函数中,这两个函数会被调用,分别启动tasklet_vec[cpu]和tasklet_hi_vec[cpu]链上的tasklet运行。

static inline void __cpu_raise_softirq(int cpu, int nr)

这个函数用来激活软中断,实际上就是第cpu号CPU的第nr号软中断的active位置1。在do_softirq()中将判断这个active位。tasklet_schedule()和tasklet_hi_schedule()都会调用这个函数。

do_softirq()有4个执行时机,分别是:从系统调用中返回(arch/i386/kernel/entry.S::ENTRY(ret_from_sys_call))、从异常中返回(arch/i386/kernel/entry.S::ret_from_exception标号)、调度程序中(kernel/sched.c::schedule()),以及处理完硬件中断之后(kernel/irq.c::do_IRQ())。它将遍历所有的softirq_vec,依次启动其中的action()。需要注意的是,软中断服务程序,不允许在硬中断服务程序中执行,也不允许在软中断服务程序中嵌套执行,但允许多个软中断服务程序同时在多个CPU上并发。


回页首

使用示例

softirq作为一种底层机制,很少由内核程序员直接使用,因此,这里的使用范例仅对其余几种软中断机制。

1.bottom half

原有的bottom half用法在drivers/char/serial.c中还能看到,包括三个步骤:

init_bh(SERIAL_BH,do_serial_bh);	//在串口设备的初始化函数rs_init()中,do_serial_bh()是处理函数
mark_bh(SERIAL_BH); //在rs_sched_event()中,这个函数由中断处理例程调用
remove_bh(SERIAL_BH); //在串口设备的结束函数rs_fini()中调用

尽管逻辑上还是这么三步,但在do_serial_bh()函数中的动作却是启动一个task queue:run_task_queue(&tq_serial),而在rs_sched_event()中,mark_bh()之前调用的则是queue_task(...,&tq_serial),也就是说串口bottom half已经结合task queue使用了。而那些更通用一些的bottom half,比如IMMEDIATE_BH,更是必须要与task queue结合使用,而且一般情况下,task queue也很少独立使用,而是与bottom half结合,这在下一节task queue使用示例中可以清楚地看到。

2.task queue

一般来说,程序员很少自己定义task queue,而是结合bottom half,直接使用系统预定义的tq_immediate等,尤以tq_immediate使用最频繁。看以下代码段,节选自drivers/block/floppy.c:

static struct tq_struct floppy_tq;	//定义一个tq_struct结构变量floppy_tq,不需要作其他初始化动作
static void schedule_bh( void (*handler)(void*) )
{
floppy_tq.routine = (void *)(void *) handler;
//指定floppy_tq的调用函数为handler,不需要考虑floppy_tq中的其他域
queue_task(&floppy_tq, &tq_immediate);
//将floppy_tq加入到tq_immediate中
mark_bh(IMMEDIATE_BH);
//激活IMMEDIATE_BH,由上所述可知,
这实际上将引发一个软中断来执行tq_immediate中挂接的各个函数
}

当然,我们还是可以定义并使用自己的task queue,而不用tq_immediate,在drivers/char/serial.c中提到的tq_serial就是串口驱动自己定义的:

static DECLARE_TASK_QUEUE(tq_serial);

此时就需要自行调用run_task_queue(&tq_serial)来启动其中的函数了,因此并不常用。

3.tasklet

这是比task queue和bottom half更加强大的一套软中断机制,使用上也相对简单,见下面代码段:

1:	void foo_tasklet_action(unsigned long t);
2: unsigned long stop_tasklet;
3: DECLARE_TASKLET(foo_tasklet, foo_tasklet_action, 0);
4: void foo_tasklet_action(unsigned long t)
5: {
6: //do something
7:
8: //reschedule
9: if(!stop_tasklet)
10: tasklet_schedule(&foo_tasklet);
11: }
12: void foo_init(void)
13: {
14: stop_tasklet=0;
15: tasklet_schedule(&foo_tasklet);
16: }
17: void foo_clean(void)
18: {
19: stop_tasklet=1;
20: tasklet_kill(&foo_tasklet);
21: }

这个比较完整的代码段利用一个反复执行的tasklet来完成一定的工作,首先在第3行定义foo_tasklet,与相应的动作函数foo_tasklet_action相关联,并指定foo_tasklet_action()的参数为0。虽然此处以0为参数,但也同样可以指定有意义的其他参数值,但需要注意的是,这个参数值在定义的时候必须是有固定值的变量或常数(如上例),也就是说可以定义一个全局变量,将其地址作为参数传给foo_tasklet_action(),例如:

int flags;
DECLARE_TASKLET(foo_tasklet,foo_tasklet_action,&flags);
void foo_tasklet_action(unsigned long t)
{
int flags=*(int *)t;
...
}

这样就可以通过改变flags的值将信息带入tasklet中。直接在DECLARE_TASKLET处填写flags,gcc会报"initializer element is not constant"错。

第9、10行是一种RESCHEDULE的技术。我们知道,一个tasklet执行结束后,它就从执行队列里删除了,要想重新让它转入运行,必须重新调用tasklet_schedule(),调用的时机可以是某个事件发生的时候,也可以是像这样在tasklet动作中。而这种reschedule技术将导致tasklet永远运行,因此在子系统退出时,应该有办法停止tasklet。stop_tasklet变量和tasklet_kill()就是干这个的。

参考资料

  1. 《Linux内核源代码情景分析》,毛德操、胡希明著,2001年9月浙江大学出版社
  2. 《Linux内核2.4版源代码分析大全》,李善平等著,2002年1月机械工业出版社
  3. 《Linux Device Drivers》,Alessandro Rubini & Jonathan Corbet,2001年8月 O'Reilly

转:Linux 2.4.x内核软中断机制的更多相关文章

  1. Linux 2.4.x内核软中断机制

    http://www.ibm.com/developerworks/cn/linux/kernel/interrupt/ 软中断概况 软中断是利用硬件中断的概念,用软件方式进行模拟,实现宏观上的异步执 ...

  2. linux 3.4.103 内核移植到 S3C6410 开发板 移植失败 (问题总结,日本再战!)

    linux 3.4.103 内核移植到 S3C6410 开发板 这个星期差点儿就搭在这里面了,一開始感觉非常不值得,移植这样的浪费时间的事情.想立刻搞定,然后安安静静看书 & coding. ...

  3. ARM linux解析之压缩内核zImage的启动过程

    ARM linux解析之压缩内核zImage的启动过程 semilog@163.com 首先,我们要知道在zImage的生成过程中,是把arch/arm/boot/compressed/head.s  ...

  4. Linux用户抢占和内核抢占详解(概念, 实现和触发时机)--Linux进程的管理与调度(二十)

    1 非抢占式和可抢占式内核 为了简化问题,我使用嵌入式实时系统uC/OS作为例子 首先要指出的是,uC/OS只有内核态,没有用户态,这和Linux不一样 多任务系统中, 内核负责管理各个任务, 或者说 ...

  5. Linux Centos 7.4 内核升级

    Linux Centos 7.4 内核升级 原始内核版本:3.10.0-693.2.2.el7.x86_64 升级内核版本:4.14.9-1.el7.elrepo.x86_64 1.导入key Key ...

  6. 深入理解Linux网络技术内幕——内核基础架构和组件初始化

    引导期间的内核选项     Linux允许用户把内核配置选项传给引导记录,再有引导记录传给内核,以便对内核进行调整.     start_kernel中调用两次parse_args,用于引导期间配置用 ...

  7. 现在的 Linux 内核和 Linux 2.6 的内核有多大区别?

    作者:larmbr宇链接:https://www.zhihu.com/question/35484429/answer/62964898来源:知乎著作权归作者所有.商业转载请联系作者获得授权,非商业转 ...

  8. Linux(Centos )的网络内核参数优化来提高服务器并发处理能力【转】

    简介 提高服务器性能有很多方法,比如划分图片服务器,主从数据库服务器,和网站服务器在服务器.但是硬件资源额定有限的情况下,最大的压榨服务器的性能,提高服务器的并发处理能力,是很多运维技术人员思考的问题 ...

  9. Linux 用户态与内核态的交互【转载】

    Linux 用户态与内核态的交互  在 Linux 2.4 版以后版本的内核中,几乎全部的中断过程与用户态进程的通信都是使用 netlink 套接字实现的,例如iprote2网络管理工具,它与内核的交 ...

随机推荐

  1. [Codeplus 4月赛]最短路

    题意:理论上是给定一张完全图,有边权,在给一些单向边求最短路. 思路: 我充分体会到了我图论的菜. 理论上建图肯定是不能\(n^2\)的,考虑如何优化呢? 将边权异或值二进制替换,最后一遍最短路就行, ...

  2. 20.multi_协程方法抓取总阅读量

    # 用asyncio和aiohttp抓取博客的总阅读量 (提示:先用接又找到每篇文章的链接) # https://www.jianshu.com/u/130f76596b02 import re im ...

  3. 牛客网NOIP赛前集训营-普及组(第七场)

    链接:C 来源:牛客网 牛牛的同学给牛牛表演了一个读心术:牛牛先任意选定一个非负整数,然后进行N次操作:每次操作前,假设牛牛当前的数是a,那么这个操作可能是a = a + x, 或者a = a * x ...

  4. P3338 [ZJOI2014]力 /// FFT 公式转化翻转

    题目大意: https://www.luogu.org/problemnew/show/P3338 题解 #include <bits/stdc++.h> #define N 300005 ...

  5. C# 中的三个高级参数 ref

    今天在浏览博文时,看到这篇文章:C#中的ref 传进出的到底是什么 ? 在传对象时使用ref的疑问 引用类型就传的就是地址,值类型传的就是值,可是还仍有那么多人迷惑,网上虽然流传着很多ref 的相关文 ...

  6. jquery.artDialog.source.js学习

    1 关键的对象关系art = jQuery = $function artDialog() {...}artDialog.fn = artDialog.prototype = artDialog.fn ...

  7. 最全Linux常用命令大全

    查看系统系统信息 arch 显示机器的处理器架构(1) uname -m 显示机器的处理器架构(2) uname -r 显示正在使用的内核版本 dmidecode -q 显示硬件系统部件 - (SMB ...

  8. java笔试之完全数计算

    完全数(Perfect number),又称完美数或完备数,是一些特殊的自然数. 它所有的真因子(即除了自身以外的约数)的和(即因子函数),恰好等于它本身. 例如:28,它有约数1.2.4.7.14. ...

  9. Linux 实用指令(8)--网络配置

    目录 网络配置 1 Linux网络配置原理图(含虚拟机) 2 查看网络IP和网关 2.1 查询虚拟网络编辑器 2.2 修改IP地址(修改虚拟网络的IP) 2.3 查看网关 2.4 查看windows环 ...

  10. vue+h-ui+layUI完成列表页及编辑页

    最近做一个新项目,用H-ui做后台, 比较喜欢他的模仿bootsharp的栅格和表单样式. 感觉不好的是iframe加载速度比较慢. 这里在原有的H-ui页面基础上加入用vue来绑数据,用的还可以. ...