2023年度盘点|2023年Linux内核十大技术革新功能
发布时间:2024-01-22 14:21:28
2023年,众多Linux内核开发者仍然在调度器、内存管理、文件系统等领域贡献着自己的idea和patch,本文从其中选取十个最典型的patchset,进行阐述,它们是:
基于eBPF的sched_ext调度类扩展
per-VMA lock
NUMA系统上kernel代码段复制
Large folios/动态大页
文件系统large block支持
基于scope的资源管理
用代理执行解决优先级反转(priority inversion)问题
延后用户空间临界区内的抢占
EEVDF调度
BPF通用迭代器
下面我们一一展开。
基于eBPF的sched_ext调度类扩展
这一patchset的开发过程,堪称神仙打架。对垒的多方,无论是发patch的还是review patch的,都是内核社区的顶流大神,甚至连看客都会北冥神功。他们之间直接的拼杀,刺刀见红,毫不留情,让凡人们见识了神仙也有性格,技术和思想的力量可以怎样无视虚伪和矫情。
sched_ext patchset由社区鼎鼎大名的Tejun Heo发出,他是Linux内核cgroup、KERNFS、PER-CPU MEMORY ALLOCATOR、WORKQUEUE等的maintainer。
这个patchset——sched: Implement BPF extensible scheduler class
Patchset的实际贡献还包括来自Google、meta、卡内基梅隆大学等多家主流厂商和科研院校的开发者。该patchset扩展了一个调度class,与之前的CFS、realtime等并行,但是它允许调度行为被一个BPF程序来实现,并声称有如下三大好处:
1.让探索和实验变地容易: 让新的调度策略可以快速迭代
2. 定制化调度行为:为特定应用定制调度器(这个调度器也许不适用于通用目的)
3. 调度器快速部署: 在产品环境下,非侵入式地修改调度器。
新加入的sched_ext与内核已经存在的stop_sched_class、dl_sched_class、rt_sched_class、fair_sched_class、idle_sched_class是一种并列关系,任何一个sched_class,都需要实现一系列的callback函数,比如:
enqueue_task:将task放入runqueue,在CFS中task会加入一个红黑树;
dequeue_task:将task从runqueue拿出来;
pick_next_task:调度时候选取下一个run的task,比如对于rt调度类而言就是找bitmap上第一个bit的queue里面的task;
task_tick:在调度tick发生时被调用,比如对于CFS而言,它会更新当前运行task的vruntime和sum_exec_runtime,并可能设置need_resched;
wakeup_preempt:一个task被唤醒的时候(也可能是调度策略或者优先级更改,比如从其他策略调整为CFS插入CFS的runqueue的switched_to_fair),可能抢占正在运行的task;
select_task_rq:比如fork一个新的task以及exec、wakeup等负载均衡场景,我们要选择把task放到哪个CPU的runqueue上面。
Patchset定义了一组可以由eBPF程序实现的callback:
并在内核的sched_ext class的callback中,调用这一组eBPF实现的callback,比如ext sched_class的select_task_rq() callback调用eBPF的select_cpu() callback:
进一步地,由于eBPF程序可以通过maps和userspace交互,实际上,调度行为也可以在userspace实现了,这让内核sched_class、eBPF的sched_ext_ops和用户空间,实现了3位一体的联动。
比如eBPF中可以pop一个BPF_MAP_TYPE_QUEUE类型的map:
而userspace则可以update_elem相关dispatch进程的pid到这个map:
整个patchset让Linux内核调度器的维护者Peter Zijlstra(同时也是ATOMIC INFRASTRUCTURE、CPU HOTPLUG、FUTEX、LKMM、MMU GATHER AND TLB INVALIDATION、Perf等的维护者)所极度反感,在patchset中直接给出了NACK:
他NAK的无疑是一位大神,当我们回眸特洛伊之战中两位伟大英雄阿喀琉斯和赫克托耳的决斗时刻,最后命运的天平无论便向的是哪一边,剩下的都只有悲壮。
但是之前我们在page fault中,也是要拿mmap_sem读锁的,因为我们也不知道page fault处理过程中,对应的VMA会不会变化或者甚至消失,所以要和可能写VMA的人排他。Page fault的处理逻辑实际是:
由于mmap_sem是整个进程的,而一个进程里面说不定也有成千上万的VMA,然后大量的page fault以及其他的VMA的写操作行为,相互竞争锁,就导致大量的竞争延迟。其他需要持有写锁的地方也是非常多的,比如:brk、stack expand、munmap、remap_file_pages、exit、madvise、mprotect、mremap、mlock等。
用一个大的mmap_lock把这些写和page fault的读进行保护,这固然安全,但是也实在低效。我们假设一个进程有1万个VMA,然后我们在其中的1个VMA上面进行page fault,其他的9999个VMA消失不消失,变化不变化,跟我这个page fault之间其实是没有半毛钱关系的。如果能够在PF中不去持有mmap_lock读锁,而去持有一个更细粒度的,只关心本VMA的锁,应该是一个更好的选择。
在处理page fault的时候,我们只需要通过持有VMA的lock,来保证这个VMA本身的稳定:
struct vm_area_struct *lock_vma_under_rcu(struct mm_struct *mm, unsigned long address);
它的这个实现看起来很奇怪,因为它拿到了vma->vm_lock->lock后,并不真地会一直拿着,而是马上就放了up_write,但是它写了一个vma->vm_lock_seq,把这个vm_lock_seq写成了vma->vm_mm->mm_lock_seq的,而进程级的mm_lock_seq会在mmap_lock释放的时候自增。
但是拿读锁的page fault,则是在page fault的途中一直hold着vma->vm_lock->lock。lock_vma_under_rcu()会调用vma_start_read():
因为我们要开始VMA写的时候把vma->vm_lock_seq写成了进程级的mm_lock_seq,这样当我们拿读锁的时候,如果vma->vm_lock_seq == mm->mm_lock_seq,说明VMA还在写,我们其实也不用拿读锁了,per-VMA读锁直接失败,让page fault的代码回退到去拿原先的mmap_lock就好。
由于per-VMA拿写锁的人总是当场放写锁,我们其实就不用担心忘记up_write了。这有点自动化的类似后面将要提到的scope-based resource management。
值得一提是,在per-VMA lock准备好之前,有些Linux内核,比如Android采用了SPF(Speculative page faults)来处理page fault,SPF的实现不包含per-VMA lock,它也不拿mmap_sem,但是page fault会不拿mmap_sem投机执行,处理过程中会边走边看,如果执行过程中发现VMA被修改,page fault会拿mmap_sem来retry原先的page fault。这个机制我们在2022年终盘点中也有提及。
NUMA系统上kernel代码段复制
Russell King,在Linux ARM体系架构采用device tree之前,维护着ARM Linux社区。由于当时的arch/arm目录充斥着大量的冗余描述硬件的代码,在2011年TI OMAP的一次Pull request中,Linus终于忍无可忍,破口大骂“this whole ARM thing is a f*cking pain in the ass”。此后,Linaro和ARM强势介入,在ARM Linux引入了device tree,开启了一个崭新的时代。自己的地盘被人革了命,Russell童鞋的黯然神伤无可掩饰。但是,作为大神,Russell无疑拥有无可辩驳的技术实力,这次他给我们带来的是黯然销魂掌arm64 kernel text replication。
在一个典型的NUMA系统中,跨node访问内存的开销比访问本地node的开销大。
Large folios/动态大页
Large folios是社区2023的热门话题,由于一个large folio中可以包含多个page,所以采用large folio可以减小page fault的次数(比如一个page fault中映射1个包含16个page的folio,这样就减少了后面15次page fault)、降低LRU的维护成本(large folio整体加入LRU)、降低内存的回收成本(large folio整体回收)等。