Linux内核设计与实现 阅读笔记:8、下半部和推后执行的工作

By:Ailson Jack

Date:2016.04.10

个人博客:www.only2fire.com

本文在我博客的地址是:http://www.only2fire.com/archives/871.html,排版更好,便于学习。

上一章简单的讲了一下中断的上半部(中断处理程序),这一章就讲讲中断的下半部以及下半部的几种实现机制,最后简单的写了几个测试的例子来测试软中断、tasklet和工作队列。

测试程序下载地址:。

1、下半部简述

中断下半部的任务是执行与中断处理密切相关但中断处理程序本身并不执行的工作。

对于中断中的任务应该在上半部还是下半部执行,并没有严格的规则来规定,但是还是有一些提示可供借鉴:

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

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

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

4)、其他所有任务可以考虑放置在下半部执行。

和上半部只能通过中断处理程序实现不同,下半部可以通过多种机制实现。随着Linux内核的发展,产生了一些新的机制,也淘汰了一些旧的机制。目前,下半部主要有三种机制:软中断,tasklet,工作队列。

2、中断下半部机制 — 软中断

软中断是一组静态定义的下半部接口,在Linux-2.6.34内核中有10个,可以在所有处理器上同时执行(即使两个类型相同也可以)。软中断必须在编译期间就进行静态注册。

软中断保留给系统中对时间要求最严格以及最重要的下半部使用。目前,只有两个子系统(网络和SCSI)直接使用软中断。此外,内核定时器和tasklet都是建立在软中断上的。

软中断的代码在:kernel/softirq.c

使用软中断的流程图如下:

1)、分配索引

软中断目前有10种类型,软中断类型定义在include/linux/interrupts.h中,可以通过修改该文件中的枚举,来实现增加或者减少软中断,下面我增加一个自己定义的软中断AilsonJack_SOFTIRQ:

enum

{

HI_SOFTIRQ=0,

TIMER_SOFTIRQ,

NET_TX_SOFTIRQ,

NET_RX_SOFTIRQ,

BLOCK_SOFTIRQ,

BLOCK_IOPOLL_SOFTIRQ,

TASKLET_SOFTIRQ,

SCHED_SOFTIRQ,

HRTIMER_SOFTIRQ,

RCU_SOFTIRQ,   /* Preferable RCU should always be the last softirq */

AilsonJack_SOFTIRQ,/*自己定义的一个新的软中断类型*/

NR_SOFTIRQS

};

接着在kernel/softirq.c文件中的softirq_to_name数组中给新增的软中断取个名字:

char *softirq_to_name[NR_SOFTIRQS] = {

“HI”, “TIMER”, “NET_TX”, “NET_RX”, “BLOCK”, “BLOCK_IOPOLL”,

“TASKLET”, “SCHED”, “HRTIMER”, “RCU”, “AilsonJack”

};/*增加一种新的softirq —> AilsonJack*/

索引号小的软中断在索引号大的软中断之前执行。

2)、注册处理程序

注册处理程序使用的是open_softirq()函数,它的定义在kernel/softirq.c文件中:

/*

* 将软中断类型和软中断处理函数加入到软中断序列中

* @nr       – 软中断类型

* @ void (*action)(struct softirq_action *) – 软中断处理的函数指针

*/

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

{

/* softirq_vec是个struct softirq_action类型的数组 */

softirq_vec[nr].action = action;

}

struct softirq_action 的定义在 include/linux/interrupt.h 文件中:

/*

* 这个结构体的成员是个函数指针,函数的名称是action

* 函数指针的返回值是void型

* 函数指针的参数是 struct softirq_action 的地址,其实就是指向 softirq_vec 中的某一项

* 如果 open_softirq 是这样调用的: open_softirq(NET_TX_SOFTIRQ, my_tx_action);

* 那么 my_tx_action 的参数就是 softirq_vec[NET_TX_SOFTIRQ]的地址

*/

struct softirq_action

{

void    (*action)(struct softirq_action *);

};

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

软中断处理程序执行的时候,允许响应中断,但它自己不能休眠。这意味着你不能在软中断中使用信号量或者其他什么阻塞式函数。

3)、触发软中断

通过在枚举类型列表中添加新项以及调用open_sofirq()进行注册以后,新的软中断处理程序就能够运行。raise_softirq()函数可以将一个软中断设置为挂起状态,让它在下一次调用do_softirq()函数时投入运行。

触发软中断的函数raise_softirq()定义在kernel/softirq.c文件中:

/*

* 触发某个类型的软中断

* @nr – 被触发的软中断类型

* 从函数中可以看出,在处理软中断前后有保存和恢复本地中断的操作

*/

void raise_softirq(unsigned int nr)

{

unsigned long flags;

local_irq_save(flags);

raise_softirq_irqoff(nr);

local_irq_restore(flags);

}

raise_softirq(NET_TX_SOFTIRQ),这样会触发NET_TX_SOFTIRQ软中断。

在中断处理程序中触发软中断是最常见的形式。

4)、执行软中断

在触发了软中断之后,系统会在合适的时刻让软中断运行,该函数不需要自己调用。

执行软中断do_softirq()定义在kernel/softirq.c文件中:

asmlinkage void do_softirq(void)

{

__u32 pending;

unsigned long flags;

/* 判断是否在中断处理中,如果正在中断处理,就直接返回 */

if (in_interrupt())

return;

/* 保存当前寄存器的值 */

local_irq_save(flags);

/* 取得当前已注册软中断的位图 */

pending = local_softirq_pending();

/* 循环处理所有已注册的软中断 */

if (pending)

__do_softirq();

/* 恢复寄存器的值到中断处理前 */

local_irq_restore(flags);

}

3、中断下半部机制 — tasklet

tasklet是用软中断实现的一种下半部机制。但是它的接口更简单,对锁保护的要求也较低。对于tasklet和软中断如何选择,通常应该选用tasklet,而对于那些执行频率很高和连续性要求很高的情况下才选用软中断。

tasklet有两类软中断类型:HI_SOFTIRQ,TASKLET_SOFTIRQ。这两者之间的唯一区别就是,HI_SOFTIRQ类型的软中断会先于TASKLET_SOFTIRQ类型的软中断执行。

tasklet由tasklet结构体表示。每个结构体单独代表一个tasklet,它定义在linux/interrupt.h文件中:

struct tasklet_struct

{

struct tasklet_struct *next;/*链表中的下一个tasklet*/

unsigned long state;/*tasklet的状态*/

atomic_t count;/*引用计数器*/

void (*func)(unsigned long);/*tasklet处理函数*/

unsigned long data;/*tasklet处理函数的参数*/

};

tasklet状态值:

0:表示tasklet没有被调度;

TASKLET_STATE_SCHED:表示tasklet已经被调度,正准备运行;

TASKLET_STATE_RUN:表示tasklet正在运行。

引用计数器count:

非0:tasklet被禁止,不允许执行;

0:tasklet被激活,可以执行。

使用tasklet的流程图如下:

1)、创建tasklet结构体

/* 静态声明一个tasklet */

#define DECLARE_TASKLET(name, func, data) \

struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(0), func, data }

#define DECLARE_TASKLET_DISABLED(name, func, data) \

struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(1), func, data }

/* 动态声明一个tasklet 传递一个tasklet_struct指针给初始化函数 */

extern void tasklet_init(struct tasklet_struct *t, void (*func)(unsigned long), unsigned long data);

2)、编写处理程序

参照tasklet处理的原型,编写自己的处理函数:

void tasklet_handler(unsigned long date)

因为tasklet是靠软中断实现的,所以taskelt也不能睡眠,即不能在tasklet中使用信号量或者其他什么阻塞式的函数。两个相同的tasklet决不会同时执行。

3)、调度tasklet

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

tasklet_sched(&my_tasklet);

和软中断一样,通常也是在中断处理程序中来调度tasklet,之后,系统会在适合的时候调度你的tasklet处理程序来对中断的下半部进行处理。

4、中断下半部机制 — 工作队列

工作队列(work queue)是另一种将工作推后执行的形式,它和我们前面讨论的所有其它形式都不同。工作队列的执行会在进程上下文中进行,这样工作队列就允许重新调度甚至是睡眠。

通常在工作队列和软中断/tasklet中做出选择非常容易。如果推后执行的任务需要睡眠,那么就选择工作队列。如果推后执行的任务不需要睡眠,那么就选择软中断或者tasklet。

缺省的工作者线程名称是 events/n (n对应处理器号)。每个处理器对应一个线程。可以使用top | grep events来查看机器上的events线程。

工作队列主要用到下面3个结构体,弄懂了这3个结构体的关系,也就知道工作队列的处理流程了。

/* 在 include/linux/workqueue.h 文件中定义 */

struct work_struct {

atomic_long_t data;  /* 这个并不是处理函数的参数,而是表示此work是否pending等状态的flag */

#define WORK_STRUCT_PENDING 0 /* T if work item pending execution */

#define WORK_STRUCT_FLAG_MASK (3UL)

#define WORK_STRUCT_WQ_DATA_MASK (~WORK_STRUCT_FLAG_MASK)

struct list_head entry; /* 中断下半部处理函数的链表 */

work_func_t func; /* 处理中断下半部工作的函数 */

#ifdef CONFIG_LOCKDEP

struct lockdep_map lockdep_map;

#endif

};

/* 在 kernel/workqueue.c文件中定义

* 每个工作者线程对应一个 cpu_workqueue_struct ,其中包含要处理的工作的链表

* (即 work_struct 的链表,当此链表不空时,唤醒工作者线程来进行处理)

*/

/*

* The per-CPU workqueue (if single thread, we always use the first

* possible cpu).

*/

struct cpu_workqueue_struct {

spinlock_t lock;  /* 锁保护这种结构 */

struct list_head worklist;  /* 工作队列头节点 */

wait_queue_head_t more_work;

struct work_struct *current_work;

struct workqueue_struct *wq;  /* 关联工作队列结构 */

struct task_struct *thread;  /* 关联线程 */

} ____cacheline_aligned;

/* 也是在 kernel/workqueue.c 文件中定义的

* 每个 workqueue_struct 表示一种工作者类型,系统默认的就是 events 工作者类型

* 每个工作者类型一般对应n个工作者线程,n就是处理器的个数

*/

/*

* The externally visible workqueue abstraction is an array of

* per-CPU workqueues:

*/

struct workqueue_struct {

struct cpu_workqueue_struct *cpu_wq; /* 工作者线程 */

struct list_head list;

const char *name;

int singlethread;

int freezeable; /* Freeze threads during suspend */

int rt;

#ifdef CONFIG_LOCKDEP

struct lockdep_map lockdep_map;

#endif

};

注意,这里有三个概念,工作(work_struct),工作者线程(cpu_workqueue_struct),工作者类型(workqueue_struct)。

这里举个例子,假设计算机有4个处理器,除了默认的events工作者类型外,我自己还新加入falcon工作者类型,对于这种情况,来说说结构体的数目。因为此时计算机上有两种工作者类型:events和falcon,那么相应的也就有2个工作者类型(workqueue_struct)结构体分别与之对应。因为计算机有4个处理器,每个处理器对应一个工作者线程,那么events工作者类型有4个工作者线程(cpu_workqueue_struct)结构体,同样的falcon工作者类型也有4个工作者线程(cpu_workqueue_struct)结构体,计算机总共有8个工作者线程(cpu_workqueue_struct)结构体。对于工作(work_struct)结构体,它就是工作者线程需要处理的任务。

使用工作队列的流程图如下:

1)、创建推后工作

创建推后执行的工作,这里有静态和动态两种方法:

/* 静态创建一个work_struct

* @n – work_struct结构体,不用事先定义

* @f – 下半部处理函数

*/

#define DECLARE_WORK(n, f)                    \

struct work_struct n = __WORK_INITIALIZER(n, f)

/* 动态创建一个 work_struct

* @_work – 已经定义好的一个 work_struct

* @_func – 下半部处理函数

*/

#ifdef CONFIG_LOCKDEP

#define INIT_WORK(_work, _func)                        \

do {                                \

static struct lock_class_key __key;            \

\

(_work)->data = (atomic_long_t) WORK_DATA_INIT();    \

lockdep_init_map(&(_work)->lockdep_map, #_work, &__key, 0);\

INIT_LIST_HEAD(&(_work)->entry);            \

PREPARE_WORK((_work), (_func));                \

} while (0)

#else

#define INIT_WORK(_work, _func)                        \

do {                                \

(_work)->data = (atomic_long_t) WORK_DATA_INIT();    \

INIT_LIST_HEAD(&(_work)->entry);            \

PREPARE_WORK((_work), (_func));                \

} while (0)

#endif

工作队列处理函数的原型:

typedef void (*work_func_t)(struct work_struct *work);

2)、刷新现有的工作

这个步骤不是必须的,可以直接进行创建推后工作和对工作进行调度这两步。

刷新现有工作的意思就是在追加新的工作之前,保证队列中的已有工作已经执行完了。

/* 刷新系统默认的队列,即 events 队列 */

void flush_scheduled_work(void);

/* 刷新用户自定义的队列

* @wq – 用户自定义的队列

*/

void flush_workqueue(struct workqueue_struct *wq);

3)、对工作进行调度

/* 调度第一步中新定义的工作,在系统默认的工作者线程中执行此工作

* @work – 第一步中定义的工作

*/

schedule_work(struct work_struct *work);

/* 调度第一步中新定义的工作,在系统默认的工作者线程中执行此工作

* @work  – 第一步中定义的工作

* @delay – 延迟的时钟节拍

*/

int schedule_delayed_work(struct delayed_work *work, unsigned long delay);

/* 调度第一步中新定义的工作,在用户自定义的工作者线程中执行此工作

* @wq   – 用户自定义的工作队列类型

* @work – 第一步中定义的工作

*/

int queue_work(struct workqueue_struct *wq, struct work_struct *work);

/* 调度第一步中新定义的工作,在用户自定义的工作者线程中执行此工作

* @wq    – 用户自定义的工作队列类型

* @work  – 第一步中定义的工作

* @delay – 延迟的时钟节拍

*/

int queue_delayed_work(struct workqueue_struct *wq,

struct delayed_work *work, unsigned long delay);

5、中断下半部小结

下面对实现中断下半部工作的3种机制进行总结,便于在实际使用中决定使用哪种机制:

下半部机制 上下文 复杂度 执行性能 顺序执行保障
软中断 中断

(需要自己确保软中断的执行顺序及锁机制)

(全部自己实现,便于调优)

没有
tasklet 中断

(提供了简单的接口来使用软中断)

同类型不能同时执行
工作队列 进程

(在进程上下文中运行,与写用户程序差不多)

没有

(和进程上下文一样被调度)

6、软中断、tasklet、工作队列的测试

这些测试程序在文章开始的地方,提供了一个下载链接,大家下载即可(重要的是那个Makefile文件,下面不会列出Makefile里面的内容)。

1)、软中断

在上面介绍软中断的类型时,自己添加了一个新的软中断类型,下面说说具体修改的内容吧:

在include/interrupt.h文件中的枚举列表中添加新的软中断类型:

在kernel/softirq.c文件中,给新添加的软中断类型命名:

导出raise_softirq()和open_softirq()函数,不然在编译模块时,会发出类似下面的警告:

WARNING: “open_softirq” undefined

WARNING: “raise_softirq”       undefined

修改好之后,就可以编译内核(make,make modules_install,make install),然后从编译好的内核启动(具体详细的编译内核的方法,可以在我的博客中搜索)。

内核之所以没有导出open_softirq和raise_softirq函数,可能还是因为提倡我们尽量用tasklet来实现中断的下半部工作

由于测试程序比较长,这里就不贴出来了,大家可以去下载的文件中看。

执行下面的步骤,对软中断进行测试:

编译模块:make

加载模块:insmod softirq.ko

卸载模块:rmmod softirq

查看信息:dmesg | tail

可以看见打印信息。

2)、tasklet

由于测试程序比较长,这里就不贴出来了,大家可以去下载的文件中看。

执行下面的步骤,对tasklet进行测试:

编译模块:make

加载模块:insmod tasklet.ko

卸载模块:rmmod tasklet

查看信息:dmesg | tail

可以看见打印信息。

3)、工作队列

由于测试程序比较长,这里就不贴出来了,大家可以去下载的文件中看。

执行下面的步骤,对工作队列进行测试:

编译模块:make

加载模块:insmod workqueue.ko

卸载模块:rmmod workqueue

查看信息:dmesg | tail

可以看见打印信息。

注:转载请注明出处,谢谢!^_^

时间: 2024-10-13 19:10:27

Linux内核设计与实现 阅读笔记:8、下半部和推后执行的工作的相关文章

《Linux内核设计与实现》笔记-1-linux内核简介

一.Linux内核相对于传统的UNIX内核的比较: (1):Linux支持动态内核模块.尽管Linux内核也是整体式结构,可是允许在需要的时候动态哦卸除(rmmod xxx)和加载内核模块(insmod  xxx.ko). (2):Linux支持对称多处理(SMP)机制,尽管许多UNIX的变体也支持SMP,但是传统的UNIX并不支持这种机制. (3):Linux内核可以抢占(preemptive).在Linux 2.4以及以前的版本都是不支持内核抢占的,在Linux 2.6以及以后就支持了. (

Linux内核设计与实现 读书笔记 转

Linux内核设计与实现  读书笔记: http://www.cnblogs.com/wang_yb/tag/linux-kernel/ <深入理解LINUX内存管理> http://blog.csdn.net/yrj/article/category/718110 Linux内存管理和性能学习笔记(一) :内存测量与堆内存 第一篇 内存的测量 2.1. 系统当前可用内存 # cat /proc/meminfoMemTotal:        8063544 kBMemFree:       

Linux内核设计与实现读书笔记——第三章

Linux内核设计与实现读书笔记——第三章 进程管理 20135111李光豫 3.1进程 1.进程即处于执行期的程序,并不局限于一个可执行的代码,是处于执行期程序以及其相关资源的总称. 2.Linux系统中,对于进程和线程并没有明显的区分,线程是一种特殊的进程. 3.Linux系统中,常用fork()进程创建子进程.调用fork()进程的成之为其子进程的父进程. 4.fork()继承实际上由clone()系统调用实现.最后通过exit()退出执行. 3.2任务描述符及任务结构 1.任务队列实质上

《Linux内核设计与实现》笔记——内核同步简介

相关概念 竞争条件 多个执行线程(进程/线程/中断处理程序)并发(并行)访问共享资源,因为执行顺序不一样造成结果不一样的情况,称为竞争条件(race condition) 举例说明 #include<thread> using namespace std; int i = 0; void thread1(){ //for(int x=0;x<100000;x++) i++; } void thread2(){ //for(int x=0;x<100000;x++) i++; } i

Linux内核设计与实现 读书笔记

第三章 进程管理 1. fork系统调用从内核返回两次: 一次返回到子进程,一次返回到父进程 2. task_struct结构是用slab分配器分配的,2.6以前的是放在内核栈的栈底的:所有进程的task_struct连在一起组成了一个双向链表 3. 2.6内核的内核栈底放的是thread_info结构,其中有指向task_struct的指针: 4. current宏可以找到当前进程的task_struct:X86是通过先找到thread_info结构,而PPC是有专门的寄存器存当前task_s

Linux内核设计与实现读书笔记——第十八章

第18章 调试 调试工作艰难是内核级开发区别于用户级开发的一个显著特点,相比于用户级开发,内核调试的难度确实要艰苦得多.更可怕的是,它带来的风险比用户级别更高,内核的一个错误往往立刻就能让系统崩溃. 18.1 准备开始 一个bug.听起来很可笑,但确实需要一个确定的bug.如果错误总是能够重现的话,那对我们会有很大的帮助(有一部分错误确实如此).然而不幸的是,大部分bug通常都不是行为可靠而且定义明确的. 一个藏匿bug的内核版本.如果你知道这个bug最早出现在哪个内核版本中那就再理想不过了.

把握linux内核设计思想(三):下半部机制之软中断

[版权声明:尊重原创.转载请保留出处:blog.csdn.net/shallnet,文章仅供学习交流,请勿用于商业用途] 中断处理程序以异步方式执行,其会打断其它重要代码,其执行时该中断同级的其它中断会被屏蔽,而且当前处理器上全部其它中断都有可能会被屏蔽掉,还有中断处理程序不能堵塞,所以中断处理须要尽快结束.因为中断处理程序的这些缺陷,导致了中断处理程序仅仅是整个硬件中断处理流程的一部分,对于那些对时间要求不高的任务.留给中断处理流程的另外一部分,也就是本节要讲的中断处理流程的下半部. 那哪些工

《Linux内核设计与实现》笔记——VFS

关于VFS有一篇很好的博客http://www.ibm.com/developerworks/cn/linux/l-vfs/ 建议先阅读本文为基础,然后继续阅读该文章. VFS,虚拟文件系统,为用户提供了文件和文件系统相关的接口. 这些接口可以跨越各种文件系统和不同介质执行. VFS提供了一个通用文件系统模型,该模型囊括了任何文件系统的常用功能集和行为. 该模型偏重于Unix风格的文件系统. 数据结构关系 如下图,下图描述了VFS相关数据结构的关系 Unix文件系统 Unix使用了4个和文件系统

linux内核设计与实现学习笔记-模块

模块 1.概念:  如果让LINUX Kernel单独运行在一个保护区域,那么LINUX Kernel就成为了“单内核”.    LINUX Kernel是组件模式的,所谓组件模式是指:LINUX Kernel在运行时,允许“代码”动态的插入或者移出Kernel.    所谓模块是指:相关的一些子程序,数据.入口点和出口点共同组合成的一个单一的二进制映像,也就是一个可装载的Kernel目标文件.    模块的支持,使得系统可以拥有一个最小的内核映像,并且通过模块的方式支持一些可选的特征和驱动程序