LWN:古老的tasklet API该何去何从?

关注了就能看到更多这么棒的文章哦~

Modernizing the tasklet API

September 14, 2020
This article was contributed by Marta Rybczyńska
原文来自:https://lwn.net/Articles/830964/
DeepL assisted translation

Tasklets 在 Linux 内核中给大家提供了一种延迟执行(deferred-execution)的方法,这个功能从 2.3 版本就已经加进来了。它可以让中断处理程序在 interrupt handler 刚刚执行完之后尽快安排执行后续工作。tasklet API 有其不足之处,不过后面不断有新的延迟执行的方法(包括 workqueue)被引入的同时,它还是得到了保留。最近,Kees Cook 发布了一个改善安全的 patch set(也包括了 Romain Perier 的一些工作)来改进 tasklet API。这个改动是没有争议的,但它引发了一场讨论,可能会导致在(不远的)未来移除 tasklet API。

对 tasklet 和其他的延迟执行机制的需求,主要来自于内核里的中断处理方式。中断(interrupt)是通常由某个硬件事件引起的。在这种事件发生时,会暂停执行当前任务,CPU 会转而执行中断处理程序 CPU。在引入 threaded interrupts 之前,中断处理程序只能执行尽量少的、必要的操作(比如访问硬件寄存器让它别再 assert 这个中断了),然后利用适当的延后执行机制来处理后续的更多工作。threaded interrupt 是从实时抢占(realtime preempt)这个功能中引入的,它会将后续的处理工作转移到一个内核线程上去,按正常的线程调度方式来执行。这个功能是在 2.6.30 内核合并的,当时 tasklets 已经很成熟了。

interrupt handler 如果看到还有一些后续工作的话,就会触发一个 tasklet 来执行。内核一有机会就会运行这个 tasklet。通常是在 interrupt handler 刚刚执行完时,或者任务返回到用户空间时。tasklet callback 是在原子操作上下文(atomic context)中运行的,也就是在软中断(software interrupt)里面,这意味着它不能 sleep,也不能访问用户空间的数据,所以不是所有的工作都可以放在 tasklet 处理程序中完成的。另外,对于某一个 tasklet 来说,在同一时刻,kernel 只允许它的一个实例(instance)运行。而多个不同的 tasklet callback 是可以同时执行的。tasklet 的这些限制在近几年新增的的延后工作机制(如 workqueue)中是不存在的。但目前的 kernel 仍然有大约一百多个地方在使用 tasklets。

Cook 的 patch set 改变了 tasklet 的 callback 的参数类型。在当前内核中,参数是一个 unsigned long,这是在初始化 tasklet 时指定的。这与内核中其他 callback 机制不同。目前内核中,首选的方式是使用指向正确类型的结构的一个指针。Cook 提出的改动就是为了按照这个习惯来改的,将 tasklet 上下文(struct tasklet_struct)传递给 callback 函数。这项工作的目的是避免一些潜在问题,比如说不需要在 callback 中将 unsigned int 转义为其他不同的类型(但是没做类型检查)。这个改动允许从 tasklet 结构中删除目前多余的 data 字段。最后,这个改动还减少了缓冲区溢出攻击的可能性,这种攻击可能会覆盖 callback 指针和 data 字段。这很可能是最主要的目标,因为这项工作最开始是发布(2019 年)在 kernel-hardening mailing list 上。

Plotting the removal of tasklets

这组 patch set 没有引起任何争议,但在 Peter Zijlstra 发表评论后,讨论的方向发生了变化,他说:"我宁愿看到 tasklets 走上渡渡鸟的道路[……]我们能不能在这里把它彻底灭绝掉?Sebastian Andrzej Siewior 在回答中建议,可以用 threaded interrupt 来代替 tasklet,因为它们也是在 atomic context 中运行的。Dmitry Torokhov 则建议用马上到期的定时器(immediately expiring timer)代替。Cook 回答说,这种改变没有办法简单地查找替换来完成,他也举了一些更复杂的使用 tasklet 的例子。其中一个例子是 AMD ccp crypto 驱动,它将 tasklets 与 DMA engine 给结合在了一起,而另一个案例是英特尔 i915 GPU 驱动,它用 tasklets 来调度 GPU 任务。

在随后的讨论中,Thomas Gleixner "勉强地" ack(表示赞同合入)了这组 patch,但也发言支持删除 tasklets。"我宁愿看到 tasklets 从地球上彻底消失,但这确实是一个大胆的壮举。" 开发者们一致认为,移除 tasklets 将是一个合理的做法,但这个任务要比改进 tasklet API 要困难的多。人们专门在 Kernel Self-Protection Project(https://kernsec.org/wiki/index.php/Kernel_Self_Protection_Project )里面为这个任务专门增加了一条。

很早以前就讨论过这个是否删除 tasklet API 的问题了,LWN 在 2007 年就有报道。当时,移除 tasklet 的主要目的是为了减少 latency(因为 tasklet 在软中断模式下运行,它们甚至可以导致最高优先级的任务被延迟)。而反对移除 tasklets 的人认为,对于需要对中断事件做出尽快响应的驱动程序来说,可能会有性能损失。那个时候,threaded interrupt 还没有合入 mainline。

在当前的内核中,tasklets 可以被 workqueue、timer 或 threaded interrupt 所取代。如果使用 threaded interrupt 的话,这个工作就只能在 interrupt handler 内执行。那些较新的延后执行机制没有 tasklets 的缺点,并且也能满足同样的需求,所以开发者们看来并没有保留 tasklets 的必要。看来,要想从 tasklets 迁移到新的机制,都只能是一个一个驱动(或子系统)地逐步完成。例如,Takashi Iwai 告知大家他已经为 sound driver 完成了删除 tasklet 的工作。

Current API changes

虽然删除 tasklet 仍然是一个长期目标,但开发者正在着手进行 API 的修改。Cook 的 patch set 对 tasklet API 进行了尽量小的修改,包括创建一个新的初始化宏,增加了一个初始化函数。在当前的内核中,tasklet 的声明方式为:

#define DECLARE_TASKLET(name, func, data) \
     struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(0), func, data }

为了与现有的用户兼容,所有对 "旧的 "DECLARE_TASKLET()的调用都改为 DECLARE_TASKLET_OLD,定义如下。

#define DECLARE_TASKLET_OLD(name, _func)        \
     struct tasklet_struct name = {             \
     .count = ATOMIC_INIT(0),                  \
     .func = _func,                          \
}

还有对 DECLARE_TASKLET_DISABLED() 这个宏进行了同样的修改。转换为 DECLARE_TASKLET_OLD()的过程看起来是可以直接机械地替换的,因为所有这些使用者都是提供了 0 作为 data 参数。

下面的 patch 包含了一个新版本的声明宏(declaration macro),它不包含这个 data 参数:

#define DECLARE_TASKLET(name, _callback)        \
     struct tasklet_struct name = {             \
     .count = ATOMIC_INIT(0),                  \
     .callback = _callback,                     \
     .use_callback = true,                       \
}

在这组新的 API 中,callback 函数被存储在 callback()字段中,而不是 func()字段。callback 本身只要简单接受一个指向 tasklet_struct 结构的指针作为它的一个参数就好:

void (*callback)(struct tasklet_struct *t);

这个结构通常会是包含在一个更大的、用户定义的结构中,此结构的指针可以通过 container_of()宏来简单获取到。patch set 还增加了一个动态(也就是在运行时)初始化一个 tasklet 的函数,其原型如下。

void tasklet_setup(struct tasklet_struct *t,
     void (*callback)(struct tasklet_struct *));

tasklet 子系统在新方式或旧的方式下都会调用这个 callback,这是根据 tasklet 如何初始化来选择的。除此之外,tasklet 的行为没有改变。

Where to from here

负责这个改动的开发者们提交了若干 patch,将内核中所有的 tasklet 初始化代码都转换为新的 tasklet_setup() 函数。如上所说,还有一项长期工作,就是删除所有这些用到 tasklet 的代码。一些子系统中已经开始这个工作了。欢迎开发者们帮助将所有子系统都转换为新的 API,并最终将所有 tasklet 调用都从内核中移除。当然,内核开发者很有意愿做这个改动,但这可能需要几个内核开发周期才能完成。

全文完
LWN 文章遵循 CC BY-SA 4.0 许可协议。

欢迎分享、转载及基于现有协议再创作~

长按下面二维码关注,关注 LWN 深度文章以及开源社区的各种新近言论~

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值