Linux 内核 RCU 补丁评审清单17 条规则的完整解读与源码实现剖析【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linux本文系统讲解 Linux 内核官方文档 Documentation/RCU/checklist.rst 中RCU 补丁制作与评审检查清单的全部 17 条规则并结合当前仓库中 include/linux/rcupdate.h、include/linux/rculist.h 等核心头文件的真实实现深入剖析每条规则背后的内存屏障机制、flavor 配对关系、回调执行模型与调试手段。读完本文你将能够在自己编写或评审任何使用 RCURead-Copy-Update的补丁时完整覆盖读侧临界区、更新侧互斥、内存排序、回调约束与模块卸载屏障等全部关键点避免与遗漏锁原语同等级别的并发缺陷。这份清单并非凭空产生文档开宗明义地说明违反其中任何一条规则都会造成与遗漏一个锁原语同类的后果而这份清单本身也是维护者长期评审 RCU 补丁经验的结晶见 checklist.rst 第 8-12 行。规则 0先回答是不是读多写少在动手用 RCU 之前第一个问题永远是该数据结构是否处于读多写少read-mostly的场景文档给出的经验阈值非常具体——如果数据结构超过约 10% 的时间都在被更新就应该强烈考虑其他方案除非详细的性能测量证明 RCU 仍是正确选择。这背后是 RCU 的基本代价模型RCU 通过增加写侧开销来降低读侧开销。读侧几乎零成本在经典 RCU 下读侧临界区只是禁止抢占/软中断但更新侧必须等待宽限期grace period才能回收旧数据。因此正常使用 RCU 的代码必然是读远多于写。文档同时列出了三个例外情形即使不满足读多写少也可以使用 RCU性能不是问题RCU 让实现更简单——典型例子是 Linux 2.6 内核中的动态 NMI 代码在 NMI 很少的架构上RCU 读侧原语的极低实时延迟至关重要用 RCU 读者防止无锁更新中的 ABA 问题。这种情况会产生一种略显反直觉的现象rcu_read_lock()/rcu_read_unlock()被用来保护更新但它能为某些无锁算法带来与垃圾回收器类似的简化效果。规则 1更新侧代码必须有真正的互斥RCU 只是豁免了读者的锁读者几乎可以裸奔但写者之间仍然必须有互斥机制。文档给出三种被认可的方案a. 加锁lockingb. 原子操作atomic operationsc. 将更新限制在单一任务中。选择 b原子操作时你必须在评审中解释如何正确处理弱序机器上的内存屏障——注意弱序机器几乎是全部机器连 x86 都允许后面的 load 重排到前面的 store 之前。选择 c单任务更新时则要说明该单任务为什么不会在大系统上成为瓶颈例如该任务只是更新关于自身、供其他任务读取的信息从定义上就不存在瓶颈。文档还提醒所谓大的定义已经变了2000 年时 8 个 CPU 就算大2017 年 100 个 CPU 已不足为奇。规则 2读侧临界区必须正确使用 rcu_read_lock() 家族RCU 读侧临界区rcu_read_lock()等原语的作用是阻止宽限期过早结束。否则你的读侧代码脚下的数据会被毫不客气地释放掉——文档原话说这会大幅增加你的内核的统计风险actuarial risk。文档给出的经验法则是任何对 RCU 保护指针的解引用都必须被以下之一覆盖rcu_read_lock()、rcu_read_lock_bh()、rcu_read_lock_sched()或合适的更新侧锁。另外两个容易被忽视的事实显式关闭抢占如preempt_disable()在效果上等价于rcu_read_lock_sched()但可读性更差而且会让 lockdep 无法检测锁问题获取一把普通自旋锁raw spinlock本身也会进入 RCU 读侧临界区。guard(rcu) / scoped_guard(rcu)更少出错的新写法文档特别介绍了guard(rcu)()和scoped_guard(rcu)()两个 C 风格守卫原语前者把当前作用域剩余部分标记为 RCU 读侧临界区后者只标记下一条语句。相比手工成对调用rcu_read_lock()/rcu_read_unlock()guard 形式不容易漏掉 unlock例如中途return。当前仓库源码中已经大量使用这种写法例如 include/rv/da_monitor.h、include/net/ip_vs.h、include/linux/bpf.h、include/linux/kallsyms.h 中都有guard(rcu)();的实际使用。两条不可逾越的红线不能依赖本代码只会在不可抢占内核编译。这类代码在启用了CONFIG_PREEMPT_COUNTy的内核中会且将会被打破RCU 保护指针泄漏出读侧临界区与指针泄漏出锁保护同等恶劣。除非在指针离开临界区之前已经安排了其他保护手段如加锁或引用计数。规则 3更新代码必须容忍并发读RCU 的全部意义就在于让读者不带锁、不做原子操作地运行——因此读者必然会在更新进行期间运行。文档按推荐程度列出四种处理方式a. 使用 RCU 变体的链表/hlist 更新原语list_*_rcu()、hlist_*_rcu()在 RCU 保护的链表上做增删替换或使用内核中其他 RCU 保护的数据结构。文档评价这几乎总是最佳方案。b. 在 a 的基础上为每个元素维护一把读写双方都获取的元素锁保护元素内部状态读者从不访问的字段可以只由更新方获取的另一把锁保护。效果同样很好。c. 让更新对读者呈现原子性。例如对对齐字段的指针更新、单个原子原语都是原子的但在锁保护下的操作序列对 RCU 读者不呈现原子性多个原子原语的序列也不原子。变通办法是把多个相关字段挪到一个独立结构体中用一层间接引用更新指针解决多字段原子性问题。可行但开始有点棘手了。d. 仔细编排更新与读取的顺序使读者在更新的每个阶段都看到有效数据。文档直言这比听起来难得多——现代 CPU 倾向于重排内存访问代码里必须大量撒布内存排序操作导致代码难以理解和测试。可行时使用smp_store_release()/smp_load_acquire()这类成对原语个别情况需要smp_mb()全屏障。文档再次建议把变化数据分组到一个独立结构体通过更新指针使其呈现原子变更通常优于手工编排。规则 4弱序 CPU 上的四项强制措施几乎所有 CPU 都是弱序的再次强调连 x86 允许后面的 load 重排到前面的 store 之前。RCU 代码必须采取以下全部措施来防止内存破坏4a. 读者用 rcu_dereference() 保证取指顺序rcu_dereference()确保 CPU先取到指针再取指针指向的数据。文档特别指出这在 Alpha 架构上是真的需要的。它还有两个附加价值优秀的文档工具让读代码的人一眼看出哪些指针受 RCU 保护防御编译器重排编译器越来越激进地重排代码rcu_dereference()也能阻止破坏性的编译器优化。文档同时警告只要足够狡黠有创意误用其返回值也是可能的详见 rcu_dereference.rst。从源码看这一原语的实现印证了文档的描述。include/linux/rcupdate.h 中的__rcu_dereference_check()宏用READ_ONCE(p)读取指针禁止编译器重排/拆分访问、通过RCU_LOCKDEP_WARN在 lockdep 下检查是否处于正确的读侧上下文、再通过rcu_check_sparse()执行 sparse 的__rcu空间检查而最终的rcu_dereference(p)只是rcu_dereference_check(p, 0)的封装rcupdate.h。list_for_each_entry_rcu()等所有_rcu()链表遍历原语内部都使用了它。注意更新侧代码使用rcu_dereference()和_rcu()遍历原语是完全合法的虽然冗余对读写共用代码特别有用但若在 RCU 读侧临界区之外使用lockdep 会抱怨——处理办法见 lockdep.rst。4b-4d. 链表/hlist 的 RCU 专用增删替换原语用list_add_tail_rcu()/list_add_rcu()插入元素、用hlist_add_head_rcu()插入 hlist 元素防止弱序机器把结构体初始化和指针种入乱序用list_del_rcu()/hlist_del_rcu()删除元素防止list_del()的指针投毒poisoning对并发读者造成毒性伤害用list_replace_rcu()/hlist_replace_rcu()在相应类型的 RCU 保护链表中以新结构替换旧结构hlist_nulls 类型的 RCU 保护链表适用类似的规则见 rculist_nulls.rst。4e. 先初始化后发布更新必须确保结构体的初始化先于指向它的指针被公开。公开一个可被 RCU 读侧临界区遍历的结构体指针时必须使用rcu_assign_pointer()。源码印证include/linux/rcupdate.hrcu_assign_pointer()的核心就是smp_store_release(p, RCU_INITIALIZER(...))——一个 release 语义的存储。这正好与 4a 中读者侧READ_ONCE()的 acquire 依赖链配合构成先看到初始化好的结构体、再看到新指针的全局顺序。内核注释还特别提醒在极少数特殊场合可以用RCU_INIT_POINTER()代替更快因为不约束 CPU 和编译器但该用rcu_assign_pointer()时错用RCU_INIT_POINTER()是一件非常糟糕的事会导致无法诊断的内存破坏。规则 5 与 6回调与同步原语的执行上下文约束规则 5如果使用了call_rcu()、call_srcu()、call_rcu_tasks()或call_rcu_tasks_trace()回调函数可能从 softirq 上下文被调用且在任何情况下都处于 bottom half 关闭状态。特别地回调不能阻塞。如果回调需要阻塞应该在回调里调度一个 workqueue 处理函数去执行那段代码对于call_rcu()queue_rcu_work()已经替你做了这件事。规则 6因为synchronize_rcu()可能阻塞它不能在任何一种 irq 上下文中调用。同样的规则适用于synchronize_srcu()、synchronize_rcu_expedited()、synchronize_srcu_expedited()、synchronize_rcu_tasks()、synchronize_rcu_tasks_rude()和synchronize_rcu_tasks_trace()。关于加急expedited变体文档给出三条明确约束加急版与非加急版语义相同但加急更消耗 CPU应仅限于罕见的配置变更操作且这些操作通常不会在实时负载运行时执行。对 IPI 敏感的实时负载可以用rcupdate.rcu_normal内核启动参数完全禁用加急宽限期可能有性能影响如果你在循环里反复调用加急原语请帮帮大家重构代码把更新批量化让一个非加急原语覆盖整个批次。这很可能比循环调用加急原语更快而且对系统其他部分尤其是其他 CPU 上的实时负载友好得多或者改用call_rcu()这类异步原语加急版会向其他 CPU 发送 IPISRCU 的加急版除外见规则 13这是它对实时负载不友好的根本原因。规则 7更新侧与读侧原语必须正确配对自 v4.20 起一个内核只实现一种 RCU flavorPREEMPTIONn时是 RCU-schedPREEMPTIONy时是 RCU-preempt。配对规则如下更新方使用call_rcu()或synchronize_rcu()时对应的读者可以使用三类原语中的任意一类rcu_read_lock()/rcu_read_unlock()任何关闭再重新开启 softirq的成对原语例如rcu_read_lock_bh()/rcu_read_unlock_bh()任何关闭再重新开启抢占的成对原语例如rcu_read_lock_sched()/rcu_read_unlock_sched()。更新方使用synchronize_srcu()或call_srcu()时读者必须使用srcu_read_lock()/srcu_read_unlock()且作用于同一个srcu_struct。加急版宽限期等待原语的规则与非加急版完全相同。RCU Tasks 家族的配对规则a. 更新方用synchronize_rcu_tasks()或call_rcu_tasks()时读者必须不做自愿上下文切换即不阻塞b. 更新方用call_rcu_tasks_trace()或synchronize_rcu_tasks_trace()时读者必须用rcu_read_lock_trace()/rcu_read_unlock_trace()c. 更新方用synchronize_rcu_tasks_rude()时读者必须使用任何关闭抢占的手段例如preempt_disable()/preempt_enable()。文档强调配错原语会导致混乱乃至坏掉的内核历史上甚至造成过可被利用的安全漏洞。因此使用不那么显而易见的配对时写注释是必须的义务。文档举了一个真实案例网络中的 XDP 功能从网络驱动的 NAPIsoftirq上下文调用 BPF 程序。BPF 的数据结构重度依赖 RCU 保护而 BPF 程序的执行完全处于 NAPI poll 周期中一段local_bh_disable()之内——这是安全的因为当更新方使用call_rcu()或synchronize_rcu()时读者可以使用任何关闭 BH 的手段。规则 8synchronize_rcu() vs call_rcu() 的选型与自限流虽然synchronize_rcu()比call_rcu()慢但它通常产生更简单的代码。除非更新性能至关重要、更新方不能阻塞、或synchronize_rcu()的延迟在用户空间可见否则应优先使用synchronize_rcu()。此外kfree_rcu()和kvfree_rcu()通常比synchronize_rcu()产生更简单的代码且没有后者毫秒级的多毫秒延迟——请在适用时充分利用它们的发射后不管fire and forget释放内存能力。call_rcu()/kfree_rcu()/kvfree_rcu()路线有一个必须人工弥补的性质synchronize_rcu()自动自限流——宽限期因任何原因延迟时更新会相应地变慢而使用call_rcu()的代码如果宽限期延迟却不限制更新速率可能造成过高的实时延迟甚至 OOM。文档给出四种获得自限流性质的办法a.计数限流统计 RCU 保护数据结构中等待宽限期结束的元素数或只统计等待延迟释放的数量强制施加上限必要时让更新阻塞等待先前延迟释放完成。阻塞更新的一种方式是在更新侧拿 mutex不要用自旋锁——其他 CPU 在锁上自旋可能让宽限期永远无法结束另一种方式是给内存分配器包一层 wrapper在等待 RCU 宽限期的内存过多时模拟 OOM。b.限制更新速率例如更新每小时只发生一次则无需显式限流。老版本的 dcache 子系统就是这么做的——用一把全局锁保护更新天然限制了速率。c.可信更新如果更新只能由超级用户或某个可信用户手动触发可能无需自动限流反正超级用户已经有很多办法把机器搞崩了。d.周期性调用rcu_barrier()允许每个宽限期只完成有限数量的更新。同样的告诫适用于call_srcu()、call_rcu_tasks()、call_rcu_tasks_trace()这也是为什么分别存在srcu_barrier()、rcu_barrier_tasks()和rcu_barrier_tasks_trace()。文档还保留了一个冷静的提醒虽然这些原语会对单个 CPU 回调堆积过多的情况采取行动以避免内存耗尽但一个坚定的用户或管理员仍然可以耗尽内存——尤其是当系统被配置为把所有 RCU 回调卸载offload到单个 CPU 上或系统可用内存本来就很少时。规则 9 与 10链表遍历原语的双向规则规则 9正向所有 RCU 链表遍历原语——包括rcu_dereference()、list_for_each_entry_rcu()、list_for_each_safe_rcu()——必须要么位于 RCU 读侧临界区内要么被合适的更新侧锁保护。读侧临界区由rcu_read_lock()/rcu_read_unlock()或类似原语如rcu_read_lock_bh()/rcu_read_unlock_bh()界定后一种情况下为了不让 lockdep 抱怨必须使用与之匹配的rcu_dereference_bh()。为什么允许在持有更新侧锁时使用遍历原语因为当读者与更新方共用同一段代码时这能显著减少代码膨胀。该场景的额外原语见 lockdep.rst。一个例外如果数据只会被添加到链式结构中、且在任何读者可能访问的时段内从不被删除则可以用READ_ONCE()代替rcu_dereference()读侧标记rcu_read_lock()/rcu_read_unlock()也可以省略。规则 10反向如果你已经处于 RCU 读侧临界区、且没有持有合适的更新侧锁你必须使用_rcu()变体的链表宏。做不到这一点会弄坏 Alpha、让激进的编译器生成错误代码、并让试图理解你代码的人困惑。规则 11 与 12RCU 回调的锁与执行模型规则 11RCU 回调获取的任何锁必须在别处以 softirq 关闭的方式获取例如spin_lock_bh()。只要某处获取该锁时没有关闭 softirq就一定会死锁——当 RCU softirq 处理程序恰好在那次获取的临界区内中断并运行你的 RCU 回调时死锁即刻发生。规则 12澄清了 RCU 回调执行的三个不要想当然回调可以且确实并行执行。很多回调只是kfree()的包装问题不大内存分配器自己的锁能处理但如果回调操作共享数据结构必须使用访问/修改该结构所需的全部同步手段不要假设回调在与call_rcu()相同的 CPU 上执行。例如某 CPU 离线时若还有挂起回调该回调会在某个存活的 CPU 上执行否则一个自我繁殖的 RCU 回调会永远阻止目标 CPU 离线。此外被rcu_nocbs启动参数指定的 CPU 很可能始终在其他 CPU 上执行回调——对某些实时负载来说这正是使用rcu_nocbs的全部目的不要假设按序入队的回调按序被调用即使它们都排在了同一个 CPU 上也不要假设同 CPU 的回调串行执行。例如在内核允许某 CPU 在卸载/不卸载回调模式间切换期间该 CPU 的回调可能既被该 CPU 的 softirq 处理程序并发执行、又被该 CPU 的 rcuo kthread 并发执行——此时回调可能并发且乱序地运行。规则 13SRCU——可睡眠的读侧以及它的代价与大多数 RCU 变体不同在 SRCU 读侧临界区srcu_read_lock()/srcu_read_unlock()界定中阻塞是允许的——这就是 SRCU 中 Ssleepable的含义。guard(srcu)()和scoped_guard(srcu)形式同样可用往往更易用。文档告诫如果读侧临界区不需要睡眠应该用 RCU 而不是 SRCU因为 RCU 几乎总是更快、更易用。SRCU 还有一个与众不同的要求显式的初始化和清理。可以在编译期用DEFINE_SRCU()、DEFINE_STATIC_SRCU()、DEFINE_SRCU_FAST()或DEFINE_STATIC_SRCU_FAST()也可以在运行期用init_srcu_struct()/init_srcu_struct_fast()加cleanup_srcu_struct()。这些原语接收一个struct srcu_struct它定义了一个 SRCU 域的作用域。初始化后这个srcu_struct会传递给srcu_read_lock()、srcu_read_unlock()、synchronize_srcu()、synchronize_srcu_expedited()和call_srcu()。这个域作用域正是可睡眠读侧能够成立的机制给定的synchronize_srcu()只等待通过同一个srcu_struct调用的srcu_read_lock()/srcu_read_unlock()所管辖的 SRCU 读侧临界区。一个子系统只会拖延自己的更新而不是其他使用 SRCU 的子系统的更新——因此 SRCU 比如果允许 RCU 读侧临界区睡眠的 RCU更不容易把系统拖进 OOM。但可睡眠不是免费的两条代价配对的srcu_read_lock()/srcu_read_unlock()必须传入同一个srcu_struct宽限期探测开销只在共享同一个srcu_struct的更新之间摊销而不是像其他 RCU 形式那样全局摊销。因此SRCU 只应在极度读密集的场景、或需要 SRCU 读侧死锁免疫/低读侧实时延迟的场景下才优先于rw_semaphore。需要轻量级读者时还应考虑percpu_rw_semaphore。此外两个相关事实SRCU 的加急原语synchronize_srcu_expedited()从不向其他 CPU 发 IPI因此比synchronize_rcu_expedited()对实时负载更友好RCU Tasks Trace 读侧临界区rcu_read_lock_trace()/rcu_read_unlock_trace()中也允许睡眠但这是专用 flavor使用前应先咨询其现有用户——多数情况下应改用 SRCU同样有guard(rcu_tasks_trace)()/scoped_guard(rcu_tasks_trace)可用。最后rcu_assign_pointer()之于 SRCU 与其之于其他 RCU 形式完全一样但为了避免 lockdep 报错解引用应该用srcu_dereference()而不是rcu_dereference()。规则 14破坏性操作之前先断开读者可达的路径call_rcu()、synchronize_rcu()及其同类原语的全部意义就是等到所有已存在的读者都结束后再执行某个破坏性操作。因此至关重要的一点是先移除任何可能受破坏性操作影响的、读者可能沿用的路径然后才调用call_rcu()/synchronize_rcu()或同类原语。由于这些原语只等待已存在的读者保证后续读者能安全执行是调用者的责任。规则 15读侧原语本身不含内存屏障各种 RCU 读侧原语不一定包含内存屏障因此应假设 CPU 和编译器会自由地把代码重排进/重排出 RCU 读侧临界区——处理这件事是 RCU更新侧原语的职责。对于 SRCU 读者可以在srcu_read_unlock()之后紧跟smp_mb__after_srcu_read_unlock()来获得完整屏障。规则 16用内核自带的调试工具验证 RCU 代码文档推荐四个验证手段各自的发现能力如下调试手段能发现的问题CONFIG_PROVE_LOCKING即 lockdep对 RCU 保护数据结构的访问是否在正确的 RCU 读侧临界区内、是否持有正确的锁组合或是否满足其他恰当条件CONFIG_DEBUG_OBJECTS_RCU_HEAD是否在同一个对象上一次call_rcu()或同类调用之后、宽限期尚未结束之前又把它传给了call_rcu()或同类CONFIG_RCU_STRICT_GRACE_PERIOD配合 KASAN 检查从 RCU 读侧临界区泄漏出去的指针。该选项对性能和可扩展性都很苛刻因此被限制在四 CPU 系统上使用__rcusparse 检查给 RCU 保护数据结构的指针标注__rcusparse 会在未经任何rcu_dereference()变体服务的情况下访问该指针时发出警告这些调试工具能帮你找到否则极其难以发现的问题。从源码结构看lockdep 的断言入口如 include/linux/rcupdate.h 中的lockdep_assert_in_rcu_read_lock()/lockdep_assert_in_rcu_read_lock_bh()/lockdep_assert_in_rcu_read_lock_sched()可以直接在代码中埋点强制检查当前是否处于对应类型的 RCU 读侧临界区而 sparse 侧的__rcu检查则由rcu_dereference()等宏内部的rcu_check_sparse()rcupdate.h在__CHECKER__编译时激活——文档第 4 条规则里更新侧误用rcu_dereference()会让 lockdep 抱怨正是这套机制在发挥作用。规则 17模块卸载必须等回调宽限期不够如果你把一个定义在模块内的回调函数传给了call_rcu()、call_srcu()、call_rcu_tasks()或call_rcu_tasks_trace()那么在卸载该模块之前必须等待所有挂起回调被调用。一个极易犯的错误认为等待一个宽限期就足够了。文档明确否定不够。例如synchronize_rcu()的实现不保证等待其他 CPU 上通过call_rcu()注册的回调——即便回调就在当前 CPU 上如果该 CPU 最近离线又上线过同样不保证。正确做法是使用对应的 barrier 函数call_rcu()→rcu_barrier()call_srcu()→srcu_barrier()call_rcu_tasks()→rcu_barrier_tasks()call_rcu_tasks_trace()→rcu_barrier_tasks_trace()而 barrier 函数又不保证等待宽限期例如当系统中没有任何call_rcu()回调排队时rcu_barrier()可以且将会立即返回。因此如果你需要既等宽限期又等所有既有回调就必须两个函数都调用配对取决于 RCU flavorsynchronize_rcu()或synchronize_rcu_expedited()加上rcu_barrier()synchronize_srcu()或synchronize_srcu_expedited()加上srcu_barrier()synchronize_rcu_tasks()加上rcu_barrier_tasks()synchronize_tasks_trace()加上rcu_barrier_tasks_trace()。必要时可以用 workqueue 之类的机制并发执行这两组函数。更多细节见 rcubarrier.rst。快速自检表在实际评审或自查一份 RCU 补丁时可以按下面的顺序过一遍场景是读多写少或有文档规则 0 列出的例外理由更新侧有互斥锁 / 原子操作 / 单任务每次解引用 RCU 指针都包在rcu_read_lock()家族 /guard(rcu)()/_rcu()遍历或更新侧锁内指针发布用rcu_assign_pointer()插入用list_*_rcu()删除用list_del_rcu()/hlist_del_rcu()更新方与读者方的原语 flavor 配对正确含 SRCU 的同一srcu_struct、Tasks 系列的禁抢占约束回调不阻塞需要阻塞走 workqueuequeue_rcu_work()加急原语只在罕见配置变更中使用循环里批量化用call_rcu()路线时有限流/计数/rcu_barrier()之类的自限流手段回调拿的锁在别处都以spin_lock_bh()方式获取不依赖回调的执行 CPU、调用顺序与串行性破坏性操作前已先切断读者可达路径模块卸载前调用了对应 barrier 函数而非仅等宽限期打开CONFIG_PROVE_LOCKING、CONFIG_DEBUG_OBJECTS_RCU_HEAD、4 CPU 系统上CONFIG_RCU_STRICT_GRACE_PERIOD KASAN 和 sparse__rcu检查跑过验证以上 13 项与 Documentation/RCU/checklist.rst 的 17 条规则一一对应配合 RCU 文档索引 Documentation/RCU/index.rst 中 what is RCU、listRCU、rcu_dereference、lockdep splat 处理 等专题文档构成了一套从使用决策到调试验证的完整 RCU 工程实践闭环。【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linux创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考