Tasklet 介绍

Linux 内核提供的四种中断下半部中 softirq(软中断)、tasklet(小任务)、workqueue(工作队列) 、request thread(中断线程)中的其中一种,其效率仅次于软中断,但远高于request thread 和 workqueue。

  • 软中断(softirq) 之所以性能高的原因,在 SMP 系统下多个 cpu 同时并发处理

如网卡的 fifo 半满中断触发,被 cpu0 处理,cpu0 会在关闭中断后,将数据从网卡的 fifo 拷贝到 ram 之后触发软中断,再打开中断,基于谁触发谁处理原则,cpu0 会继续执行软中断服务函数。若网卡的 fifo 全满中断有再次触发,就会被 cpu1 处理,同样是关闭中断后拷贝数据再开启中断,再去触发和执行软中断进行网卡数据包处理。若此时 cpu0\cpu1 都还在软中断处理数据,网卡再次产生中断,那么 cpu2 就会继续相同的流程。由此可见,软中断充分利用的多 cpu 进行并发处理,因此性能非常高,但也同时因为并发的存在,就需要考虑临界区的问题。

  • 小任务(tasklet) 之所以性能较软中断差,是因为同一种小任务在多个 cpu 上不会并发执行

由于 tasklet 基于 softirq 的基础实现,为了易用性考虑,同一种 tasklet 在多个 cpu 上不会并行执行,因此不存在并发问题,在使用上就可以少一些顾虑。但也正是因为不存在并发,导致了性能较之 softirq 差一些。

  • tasklet 之所以比 workqueue 和 request thread 性能高

原因是因为前者是在软中断上下文件工作(意味着不能调用任何阻塞的接口),而后两者是在进程上下文工作(实质上是在内核线程里面执行)。

使用示例模版

#include <linux/module.h>
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/interrupt.h> static struct tasklet_struct my_tasklet; static void my_tasklet_handle(unsigned long data)
{
printk("tasklet handle running...\n");
} static irqreturn_t xxx_interrupt(int irq, void *dev_id)
{
// 调度 tasklet
tasklet_schedule(&my_tasklet);
} static int __init demo_driver_init(void)
{
// 初始化一个 tasklet ,关联处理函数
tasklet_init(&my_tasklet, my_tasklet_handle, 0);
request_irq(xxx, xxx_interrupt, IRQF_SHARED, xxx, xxx);
return 0;
} static void __exit demo_driver_exit(void)
{
tasklet_kill(&my_tasklet);
return ;
} module_init(demo_driver_init);
module_exit(demo_driver_exit);
MODULE_LICENSE("GPL v2");

内核源码分析:

Linux 内核被启动后,会执行 start_kernel() 函数,tasklet 是基于 softirq 实现的,会在 softirq_init() 里面进行必要的初始化,主要是初始化 tasklet 链表和与相应的软中断号建立关联。

tasklet_hi_action 是高优先级的 tasklet,tasklet_action 是普通的 tasklet,两者实现原理都一样。

// kernel\linux-4.9\init\main.c
asmlinkage __visible void __init start_kernel(void)
{
...
softirq_init();
...
} // kernel\linux-4.9\kernel\softirq.c
void __init softirq_init(void)
{
int cpu; // 初始化链表
for_each_possible_cpu(cpu) {
per_cpu(tasklet_vec, cpu).tail =
&per_cpu(tasklet_vec, cpu).head;
per_cpu(tasklet_hi_vec, cpu).tail =
&per_cpu(tasklet_hi_vec, cpu).head;
} // 建立TASKLET_SOFTIRQ、HI_SOFTIRQ软中断号的对应服务接口
open_softirq(TASKLET_SOFTIRQ, tasklet_action);
open_softirq(HI_SOFTIRQ, tasklet_hi_action);
}

当调用 tasklet_schedule() 时,如果该 tasklet 没有被设置 TASKLET_STATE_SCHED 标记时,才会加入链表内,如果已经设置了 TASKLET_STATE_SCHED 了,那么就会忽略此次的 tasklet _schedule(),这就意味着如果在极短的时间内调用 tasklet_schedule() 只会触发一次(这里可能会存在丢失中断事件的情况)。之后会通过 raise_softirq_irqoff() 启用 TASKLET_SOFTIRQ 软中断。

哪颗 cpu 受理该软中断,就将 tasklet 加入到该 cpu 的 tasklet 链表内,由于相同的软中断可以同时被其他 cpu 触发执行,因此会出现 cpu0\cpu1 的 tasklet 链表内有同一个 tasklet 的情况。

// kernel\linux-4.9\include\interrupt.h
static inline void tasklet_schedule(struct tasklet_struct *t)
{
if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state))
__tasklet_schedule(t);
} // kernel\linux-4.9\kernel\softirq.c
void __tasklet_schedule(struct tasklet_struct *t)
{
unsigned long flags; // 将指定的 tasklet 加入到链表内并设置软中断
local_irq_save(flags);
t->next = NULL;
*__this_cpu_read(tasklet_vec.tail) = t;
__this_cpu_write(tasklet_vec.tail, &(t->next));
raise_softirq_irqoff(TASKLET_SOFTIRQ);
local_irq_restore(flags);
}

在初始化的时候已经为  TASKLET_SOFTIRQ 软中断与 tasklet_action() 建立关联的关系,因此软中断触发时,就会调用 tasklet_action(),这里是精髓部分。

多次触发软中断时,当前 cpu 只能同一时间执行一次相同的软中断,但是如果有多个 cpu 的话,那么会有多个 cpu 并发执行软中断,所以下面的 tasklet_action() 要想到这一点。

1. tasklet_action() 屏蔽当前 CPU 中断,获取当前 CPU 的 tasklet 链表并清空原有的链表,在恢复中断。避免在操作链表的过程中,被硬件中断打断。

2. 开始遍历链表取出 tasklet,先 TASKLET_STATE_RUN 标记确定该 tasklet 是否已经被其它 cpu 执行,因为该 tasklet 在执行的过程中,又被加入到当前的 cpu 的 tasklet 链表内。

3. 如果没有被执行,就继续检查该 tasklet 是否被 tasklet_disable(),如果有被 disable 就清除 TASKLET_STATE_RUN 标记,这样可以重新被添加会当前 tasklet 链表内,等待再次执行。如果有被执行,即使又被设置了 TASKLET_STATE_RUN 标记,也会在第 5 步执行完成后,会清除掉该标记。

4. 如果没有被 disable,那么就清除 TASKLET_STATE_SCHED 标记,该标记一旦被清除,就意味着在 tasklet 执行期间,tasklet_schedule() 可以继续添加新的 tasklet 其他 cpu  的 tasklet 链表内。如果当前 cpu 已经完成了 tasklet_action() ,新的 tasklet 也可能会重新添加到当前的  tasklet 链表。

5. 这里就会执行通过 tasklet_init() 绑定的 func,也就是示例中的 my_tasklet_handle(),执行完成后再清除 TASKLET_STATE_RUN 标记,继续下一个 tasklet。

6. 能走到这一步,会有两种情况,一种情况是即将执行 tasklet ,发现已经被 disable 掉了,另外一种情况是 tasklet 已经在其它 CPU 上执行中。无论哪种情况,都会将当前的 tasklet 重新放回到当前 cpu 的 tasklet 链表内,并调用 __raise_softirq_irqoff() 重新触发软中断(应该是启用该软中断)。

注意,以上获取 TASKLET_STATE_RUN 和 TASKLET_STATE_SCHED 标记都是位原子操作,所以不会出现因并发引发的问题。

// kernel\linux-4.9\kernel\softirq.c
// 某 CPU 要调度各个 tasklet 的实现
static __latent_entropy void tasklet_action(struct softirq_action *a)
{
struct tasklet_struct *list; // 1 ------------------------------------------------------
local_irq_disable();
list = __this_cpu_read(tasklet_vec.head); // 获取 tasklet 链表
__this_cpu_write(tasklet_vec.head, NULL); // 清空 tasklet 链表
__this_cpu_write(tasklet_vec.tail, this_cpu_ptr(&tasklet_vec.head));
local_irq_enable(); // 如果存在 tasklet 就会进入循环
while (list)
{
struct tasklet_struct *t = list;
list = list->next; // 2 ------------------------------------------------------
// TASKLET_STATE_SCHED: 表示该 tasklet 已经被挂接到某个 CPU 上
// TASKLET_STATE_RUN: 表示该 tasklet 正在某个 CPU 上执行
// 检查并设置 TASKLET_STATE_RUN 标记
// 返回 1: 表示 tasklet 没有被执行 返回 0: 表示 tasklet 已经被执行
if (tasklet_trylock(t))
{ // 3 ------------------------------------------------------
// 如果当前 tasklet 没有被 tasklet_disable()
if (!atomic_read(&t->count))
{
// 4 ----------------------------------------------
// 清除 TASKLET_STATE_SCHED 状态,便于该 tasklet 可以再次被触发
if (!test_and_clear_bit(TASKLET_STATE_SCHED, &t->state))
BUG();
// 5 ----------------------------------------------
// 这期间,该 tasklet 可以被 tasklet_schedule(),从而引出下面第二种情况
t->func(t->data); // 如果没有执行且没有 disable 则执行
tasklet_unlock(t); // 清理 TASKLET_STATE_RUN 标记
continue; // 继续下一个 tasklet
} // 如果当前已经被 disable 了,那就清理 TASKLET_STATE_RUN 标记
tasklet_unlock(t);
} // 6 -----------------------------------------------------
// 有两种情况下,会将该 tasklet 再挂接回链表内,并重新触发,等待下一次执行的机会
// 1. 如果没有被执行,但是被调用 tasklet_disable() 接口 disable 了
// 2. 当前 tasklet 已经在其它 CPU 正在执行 func 这时候 tasklet 又会被挂回在
// 原来的链表中,为了满足同一种类型的 tasklet 只能在一个 CPU 上执行的设计
// 因此此次不执行 tasklet,挂入链表后等待下一次被执行的时机执行
local_irq_disable();
t->next = NULL;
*__this_cpu_read(tasklet_vec.tail) = t;
__this_cpu_write(tasklet_vec.tail, &(t->next));
__raise_softirq_irqoff(TASKLET_SOFTIRQ);
local_irq_enable();
}
}

贴出判断和标记和清除 tasklet 运行的 TASKLET_STATE_RUN  标记代码

// kernel\linux-4.9\include\interrupt.h
static inline int tasklet_trylock(struct tasklet_struct *t)
{
return !test_and_set_bit(TASKLET_STATE_RUN, &(t)->state);
} // kernel\linux-4.9\include\interrupt.h
static inline void tasklet_unlock(struct tasklet_struct *t)
{
smp_mb__before_atomic();
clear_bit(TASKLET_STATE_RUN, &(t)->state);
}

贴出关闭 tasklet 执行的代码

// kernel\linux-4.9\include\interrupt.h
static inline void tasklet_disable(struct tasklet_struct *t)
{
tasklet_disable_nosync(t);
tasklet_unlock_wait(t);
smp_mb();
} // kernel\linux-4.9\include\interrupt.h
// 关闭 tasklet(实际上应该理解为暂停调度执行)
static inline void tasklet_disable_nosync(struct tasklet_struct *t)
{
// 这里对整型原子操作 count 自增了,对应 tasklet_action() 里面的 atomic_read()
atomic_inc(&t->count);
smp_mb__after_atomic();
} // kernel\linux-4.9\include\interrupt.h
static inline void tasklet_enable(struct tasklet_struct *t)
{
// 这里对整型原子操作 count 自减了,对应 tasklet_action() 里面的 atomic_read()
smp_mb__before_atomic();
atomic_dec(&t->count);
}

tasklet_init() 实现与用户函数关联的接口,也是很简单

// kernel\linux-4.9\kernel\softirq.c
void tasklet_init(struct tasklet_struct *t,
void (*func)(unsigned long), unsigned long data)
{
t->next = NULL;
t->state = 0;
atomic_set(&t->count, 0);
t->func = func;
t->data = data;
}

tasklet_kill() 主要实现是尽可能快的让 tasklet 得到执行,等待执行完成后再退出。

1. 判断该 tasklet 是否已经被挂接到某个 cpu 的 tasklet 链表内,如果有挂接到,那么就立即让出 cpu,直至 tasklet 清除 TASKLET_STATE_SCHED 标记(参考 tasklet_action() 第 4 个步骤),进入运行状态。

2. 如果 tasklet 没有挂接或者一旦进入到执行状态,那么就会不停的检测 TASKLET_STATE_RUN 是否有被清除,被清除的话说明已经运行完成(参考 tasklet_action() 第5个步骤),可以放心的退出了。

// kernel\linux-4.9\kernel\softirq.c
void tasklet_kill(struct tasklet_struct *t)
{
if (in_interrupt())
pr_notice("Attempt to kill tasklet from interrupt\n"); // 1 ----------------------------------------------
while (test_and_set_bit(TASKLET_STATE_SCHED, &t->state)) {
do {
yield();
} while (test_bit(TASKLET_STATE_SCHED, &t->state));
}
// 2 ----------------------------------------------
tasklet_unlock_wait(t);
clear_bit(TASKLET_STATE_SCHED, &t->state);
} // kernel\linux-4.9\include\interrupt.h
static inline void tasklet_unlock_wait(struct tasklet_struct *t)
{
while (test_bit(TASKLET_STATE_RUN, &(t)->state)) { barrier(); }
}

至此,tasklet 关键的实现原理分析完成,实际上还应联合 softirq,才算完整的了解整个机制。

Tasklet 机制总结

1. 每颗 cpu 都有自己的 tasklet 链表,这样可以将 tasklet 分布在各个 cpu 上,可实现并发不同的 tasklet。

2. 相同的 tasklet 只能在某一颗 cpu 上串行执行,其它 cpu 会暂时避让,在此情况下,不需要考虑并发问题(即不需要加锁)。

3. tasklet_schedule() 接口调用时,如果 tasklet 还未被执行,或者处于 disable 期间,指定的 tasklet 不会被加入链表内,即该请求不会被受理。

4. tasklet_disable() 接口只是暂时停止指定的 tasklet 执行,依然会被加回待执行链表内。而在 disable 期间,相同的 tasklet 将无法被加入链表调度。

参考资料:http://www.wowotech.net/irq_subsystem/tasklet.html

【分析笔记】Linux tasklet 机制的理解的更多相关文章

  1. linux中的tasklet机制【转】

    转自:http://blog.csdn.net/yasin_lee/article/details/12999099 转自: http://www.kerneltravel.net/?p=143 中断 ...

  2. Linux程序设计学习笔记——异步信号处理机制

    转载请注明出处: http://blog.csdn.net/suool/article/details/38453333 Linux常见信号与处理 基本概念 Linux的信号是一种进程间异步的通信机制 ...

  3. 《linux 内核全然剖析》sched.c sched.h 代码分析笔记

    版权声明:本文为博主原创文章.未经博主同意不得转载. https://blog.csdn.net/u011368821/article/details/25129835 sched.c sched.h ...

  4. 【原创】Linux信号量机制分析

    背景 Read the fucking source code! --By 鲁迅 A picture is worth a thousand words. --By 高尔基 说明: Kernel版本: ...

  5. 嵌入式Linux内核tasklet机制(附实测代码)

    Linux 中断编程分为中断顶半部,中断底半部 中断顶半部: 做紧急,耗时短的事情,同时还启动中断底半部. 中断底半部: 做耗时的事件,这个事件在执行过程可以被中断. 中断底半部实现方法: taskl ...

  6. Linux内核分析第七周学习笔记——Linux内核如何装载和启动一个可执行程序

    Linux内核分析第七周学习笔记--Linux内核如何装载和启动一个可执行程序 zl + 原创作品转载请注明出处 + <Linux内核分析>MOOC课程http://mooc.study. ...

  7. 《linux 内核全然剖析》 fork.c 代码分析笔记

    fork.c 代码分析笔记 verifiy_area long last_pid=0; //全局变量,用来记录眼下最大的pid数值 void verify_area(void * addr,int s ...

  8. Android源码分析笔记--Handler机制

    #Handler机制# Handler机制实际就是实现一个 异步消息循环处理器 Handler的真正意义: 异步处理 Handler机制的整体表述: 消息处理线程: 在Handler机制中,异步消息处 ...

  9. Linux信号机制

    Linux信号(signal) 机制分析 [摘要]本文分析了Linux内核对于信号的实现机制和应用层的相关处理.首先介绍了软中断信号的本质及信号的两种不同分类方法尤其是不可靠信号的原理.接着分析了内核 ...

  10. Windows五种IO模型性能分析和Linux五种IO模型性能分析

    Windows五种IO模型性能分析和Linux五种IO模型性能分析 http://blog.csdn.net/jay900323/article/details/18141217 http://blo ...

随机推荐

  1. pod(七):静态pod

    目录 一.系统环境 二.前言 三.静态pod 3.1 何为静态pod 3.2 创建静态pod 3.2.1 使用--pod-manifest-path指定静态pod目录 3.2.2 静态pod默认目录/ ...

  2. .NET深入了解哈希表和Dictionary

    引子 问题:给定一串数字{1,2,5,7,15,24,33,52},如何在时间复杂度为O(1)下,对数据进行CURD? 数组:我创建一个Length为53的数组,将元素插入相同下标处,是不是就可以实现 ...

  3. i春秋Do you know upload?

    打开题目是一个文件上传,就先写了一个一句话木马的php文件,直接提交显示文件类型不允许.于是乎将其改为jpeg格式上传,成功了,但是没用,菜刀连不上.再次上传jpg格式的一句话木马(写好php木马后将 ...

  4. Chrome 103支持使用本地字体,纯前端导出PDF优化

    在前端导出PDF,解决中文乱码一直是一个头疼的问题.要解决这个问题,需要将ttf等字体文件内容注册到页面PDF生成器中.但是之前网页是没有权限直接获取客户机器字体文件,这时就需要从服务器下载字体文件或 ...

  5. Day24:static关键字

    static static关键字是静态的意思,可以修饰成员方法.属性. static修饰的特点: 被类的所有对象共享 可以通过类名调用,也可以通过对象名调用:推荐使用类名调用! public clas ...

  6. GO开发工具litelDE的安装与使用

    1.MinGW的下载与安装 地址:http://sourceforge.net/projects/mingw/ 下载安装 D:\Program Files\MinGW 然后打开D:\Program F ...

  7. redis集群之分片集群的原理和常用代理环境部署

    上篇文章刚刚介绍完redis的主从复制集群,但主从复制集群主要是为了解决redis集群的单点故障问题,通过整合哨兵能实现集群的高可用:但是却无法解决数据容量以及单节点的压力问题,所以本文继续介绍red ...

  8. Blazor 部署 pdf.js 不能正确显示中文资源解决办法

    在Blazor项目嵌入 pdf.js 时不能正确显示中文,浏览器F12显示如下错误 错误 l10n.js /web/locale/locale.properties not found. 我找到了解决 ...

  9. Gepetto:使用chatGPT来对函数功能进行分析并重命名变量的IDA插件

    最近OpenAI的chatGPT很火,chatGPT是一个大型的语言模型,能够生成人类语言的文本,主要用于对话式的问答和聊天,以及模拟人类的对话行为 有关chatGPT的介绍就不多赘述了,相关内容很多 ...

  10. 二阶段目标检测网络-FPN 详解

    论文背景 引言(Introduction) 特征金字塔网络 FPN FPN网络建立 Anchor锚框生成规则 实验 代码解读 参考资料 本篇文章是论文阅读笔记和网络理解心得总结而来,部分资料和图参考论 ...