关注了就能看到更多这么棒的文章哦~
Migration disable for the mainline
By Jonathan Corbet
November 9, 2020
DeepL assisted translation
https://lwn.net/Articles/836503/
realtime 的开发人员多年来一直在努力创建一个能够确保最高优先级的任务总是能够毫不延迟地运行的内核。这意味着需要进行漫长的工作,不断寻找和解决那些可能会阻止高优先级任务运行的情况。其中,有一个长期存在的问题就是那些关闭了抢占(disable preemption)的内核代码。realtime 开发人员已经实现的一个工具就是禁用 migration(迁移,指将某个进程从一个 CPU 移动到另一个 CPU),而不是禁用抢占。不过这种方法在 scheduler 开发人员中并未广泛接受。即便如此,最终的解决方案似乎也是来自 scheduler 开发者 Peter Zijlstra 的这套 migration-disable patch set (https://lwn.net/ml/linux-kernel/20201023101158.088940906@infradead.org/)。
内核中使用的可扩展性技术之中非常关键的一个,是 per-CPU 数据。全系统级别的锁(system-wide locking)是保护共享数据的一种很有效的方式,但因为多种原因,它会损害性能,哪怕这个锁本身没有多少争抢也一样。任何只被单个 CPU 访问的数据结构都不需要全系统级别的锁保护,从而避免了这个问题。举例来说,内存分配器(memory allocator)会维护每个 CPU 的可用内存区域的列表,在分配的时候就可以不受系统上其他 CPU 的干扰了。但是,内核代码只有在独占 CPU 的情况下才能安全地操作 per-CPU 数据,如果这时其他进程有渠道可以插进来执行,那么它可能会看到(或导致)产生不一致的 per-CPU 数据结构。要防止这种情况的话, 正常来说就是在必要的时候禁用抢占,这个操作开销非常小(本质上来说就是设置一个标志而已),这样就能确保一个给定的任务在完成工作之前不会被中断。
禁用抢占,这是 realtime 开发者不喜欢看到的,他们已经进行了大量的工作来确保任何 task 都可以在更高优先级的 task 需要 CPU 时被中断。当他们努力移除这些 preemption-disabled 区域的时候,他们观察到,通常情况下,真正需要的是防止 task 在访问 per-CPU 数据时在 CPU 之间移动,也许还需要加上一些(通常是 本地 CPU 的)锁。例如,参见 kmap_local()的工作(https://lwn.net/Articles/836144/)。禁用迁移的话,仍然可以允许进程被抢占,所以它不会破坏 realtime project 的目标,或者说那些开发者希望能够达到这个目的。
不过,禁用迁移也会带来自己特有的问题。内核的 CPU scheduler 的任务是充分利用系统中所有的 CPU。如果有 N 个 CPU 可用,它们应该在任何时候都在运行 N 个最高优先级的任务。如果不偶尔在 CPU 之间移动任务的话,这个目标就无法实现。每次新创建的任务都恰好能落在正确的处理器上的话,那就太好了,但现实世界不是这样的。因此,如果剥夺了调度器迁移任务的能力的话,哪怕只是暂时关闭,也实际上剥夺了一个对系统的整体行为和吞吐量至关重要的工具。
举个简单的例子,想象一个只有两个 CPU 和两个任务的系统,其中只有优先级较低的任务当前是 runnable 状态,也就是可以真正执行的。在高优先级任务在这个 CPU 上变成 runnable 状态的同时,这个低优先级任务就进入了一段 mirgration-disabled 的阶段。该低优先级任务将被抢占,以便让高优先级任务能够运行。不过,这时低优先级任务仍然希望使用 CPU,而与此同时,另一个 CPU 却处于空闲状态。正常情况下,调度器会将低优先级任务迁移到空闲的 CPU 上,让它继续运行,但由于这里已经暂时禁用了迁移,所以此进程就被 block 住,无法运行。因此,这是 migration disable 和 preemption disable 的一个区别,如果只是禁用抢占的话,不会导致此进程被阻塞。
因此,不出意外,这个 migration disable 功能在 mainline 的 scheduler 开发者们之中并不受欢迎。不过,这些开发者(尤其是 Zijlstra)也明白推动这项工作的真正需求是什么。所以,当 Thomas Gleixner 在 9 月份发布了一套 migration-disable 的补丁时,Zijlstra 拒绝合入它,但他也着手创建了一个从 scheduler 角度来看可以接受的替代方案,至少对 realtime 内核应该够用了。实现这个机制的 patch,一上来的 comment 里面就明确指出,这种 migration disable 功能是"(强烈)不希望采用的":
这充其量只是一个 "临时 "的变通方法。正确的解决方案是摆脱上述假设,并重新设计代码,采用显式的 per-cpu locking 或更短的 preempt-disable 区域。
最终的目标必须是摆脱 migrate_disable(),否则的话我们就需要一个不依赖于强制迁移的可调度性方式了。
在让 migration disable 功能能正常工作起来时,有几个特别棘手的地方。其中之一,自然是 CPU hotplug,这在过去已经证明 hotplug 有很多麻烦(https://lwn.net/Articles/537562/)。如果要将 CPU 从系统中移除,首先应该将所有正在运行的进程迁移到其他地方,以免碰到更棘手的问题,也就是发怒的用户。但如果其中一些进程已经禁用了迁移功能,那就不能立即进行迁移。所以热插拔机制必须要能统计出每个 runqueue 中有多少任务禁用了迁移,并且要等到这个数字降到零。
然后,就是上面所说的任务被阻塞的问题:可能有一个 CPU 可用来运行一个优先级较低的任务,而这个任务已经被抢占掉了,在禁用迁移后,这个任务就无法移动到这个可用的 CPU 上。在极端的情况下,几个被抢占的任务最终可能都会堆积在一个 CPU 上,无法迁移,而大部分系统仍然处于空闲状态。这种行为违反了"work conservation"的规则,肯定不会让调度器开发人员开心——他们已经因为暴躁而很出名了。
针对这个问题所采取的方法并不是一个完美的解决方案(可能并不存在一个完美的方案),但希望它能有所帮助。如果一个 CPU 的 runqueue 中包含一个 runnable 的任务,但它已经被一个更高优先级的任务抢占了,正常的反应是尝试将这个被抢占的任务迁移到其他地方。如果迁移已经被禁止,显然这是不可能的。所以,调度器会尝试迁移正在运行的、优先级更高的任务,以解除这种困境。这并不是个理想的方案,迁移是有代价的,包括可能会导致损失 cache locality(CPU 本地 cache 中已有数据的高效访问),这些代价现在将由更高优先级的任务来承担。或者,正如 Zijlstra 所说的那样。"这给更高优先级的任务增加了迁移带来的干扰,但是却使得系统完全的计算能力得以恢复,否则就得不得不承担算力损失了"。
最后,值得指出的是,迁移禁用将仅限于配置为 realtime 的内核。在其他内核上,调用 migrate_disable()将会正常禁用抢占,就像现在的行为一样。所以对于大多数用户来说,行为不会改变,至少不会直接改变。在这么多年后,这是将 realtime preemption patch 完全迁移到 mainline 上的又一重要步骤。
全文完
LWN 文章遵循 CC BY-SA 4.0 许可协议。
欢迎分享、转载及基于现有协议再创作~
长按下面二维码关注,关注 LWN 深度文章以及开源社区的各种新近言论~

本文围绕内核中迁移禁用(migration disable)功能展开。realtime开发人员为确保高优先级任务运行,实现禁用迁移而非禁用抢占,但该方法未获scheduler开发人员广泛接受。禁用迁移虽不破坏realtime目标,但会带来任务阻塞等问题,最终目标是摆脱该功能,且迁移禁用将限于realtime内核。
1410

被折叠的 条评论
为什么被折叠?



