:local_irq_lock)
文章目录1. 前言2. local_irq_lock3. 参考资料1. 前言限于作者能力水平本文可能存在谬误因此而给读者带来的损失作者不做任何承诺。2. local_irq_locklocal_irq_lock是CPU 本地并发控制机制不适用于CPU 间的并发控制。PREEMPT_RT 内核将 local_irq_lock 实现为 per-CPU 的 spinlock_t从而改变了语义所有对 spinlock_t 的更改也适用于 local_irq_lock。由于 PREEMPT_RT 特有的 spinlock_t 语义local_irq_lock 不适用于在 PREEMPT_RT 内核上防止抢占或中断。在 PREEMPT_RT 内核中软中断上下文是可抢占的因为软中断在线程线程文处理若通过隐式上下文对每个禁用下半部分的代码段进行同步将导致形成一个隐式的 per-CPU 的大内核锁。PREEMPT_RT 内核引入local_irq_lock的目的是替换local_irq_*()调用。非 PREEMPT_RT 内核和PREEMPT_RT 内核中local_irq_*()的语义均为禁用当前 CPU 上的中断同时禁用中断隐式地意味着也禁用了 CPU 上的抢占如下代码所示#definelocal_irq_disable()do{raw_local_irq_disable();}while(0)#definelocal_irq_save(flags)\do{\raw_local_irq_save(flags);\}while(0)既然PREEMPT_RT 内核和非 PREEMPT_RT 内核中local_irq_*()的语义是相同的那为什么还要引入local_irq_lock来替换local_irq_*()呢在PREEMPT_RT 内核下为了更好的实时响应速度应尽量减少禁用抢占调度的代码比例local_irq_*()保护的临界区代码禁用中断导致间接的禁用了抢占所以在PREEMPT_RT 内核下也是可能需要调整的目标之一。引入local_irq_lock的目的就在于此在一些场合下既能保证原来非 PREEMPT_RT 内核下local_irq_*()保护的临界区代码在PREEMPT_RT 内核下也能对数据正确的并发访问又能避免该临界区耗时过长长期无法调度的情形。来看PREEMPT_RT 内核下local_irq_lock的实现细节看它是如何实现前述目标的PREEMPT_RT 内核下将local_irq_lock实现为perc-CPU 的 spinlock本文基于Linux 4.14.336 patches-4.14.336-rt159分析// include/linux/locallock.h/* * per cpu lock based substitute for local_irq_*() */structlocal_irq_lock{spinlock_tlock;/* 用 spinlock 实现同步 */structtask_struct*owner;/* 锁当前的持有者 */intnestcnt;/* 嵌套层次 */unsignedlongflags;/* 保存中断状态标志, 因为 local_irq_lock 不会禁用中断, 所以恒设为 0 */};通过DEFINE_LOCAL_IRQ_LOCK()定义local_irq_lock#defineDEFINE_LOCAL_IRQ_LOCK(lvar)\DEFINE_PER_CPU(structlocal_irq_lock,lvar){\.lock__SPIN_LOCK_UNLOCKED((lvar).lock)}然后通过local_irq_lock_init()初始化#definelocal_irq_lock_init(lvar)\do{\int__cpu;\for_each_possible_cpu(__cpu)\spin_lock_init(per_cpu(lvar,__cpu).lock);\}while(0)在PREEMPT_RT 内核下将local_irq_lock实现为per-CPU spinlock后因为PREEMPT_RT 内核下 spinlock 并不禁用抢占所以使用 spinlock 实现的local_irq_lock保护的临界区代码就可以调度了这提高了实时性同时因为local_irq_lock实现为per-CPU spinlock所以可以正确的进行 CPU 本地并发控制即和本地 CPU 上的中断、软中断、进程正确的并发访问相同数据PREEMPT_RT 内核下中断和软中断都线程化了如果原本非 PREEMPT_RT 内核下local_irq_*()保护的临界区数据不会和其它 CPU 并发访问那么就可以PREEMPT_RT 内核下将原本的local_irq_*()替换为local_irq_lock的local_lock()系列 API 接口了。当然如果不满足前述条件也就不能将local_irq_*()替换掉了。来看几个例子只管的感受下。释放单个 page 到 PCP(Per-CPU-Page)// mm/page_alloc.cstaticDEFINE_LOCAL_IRQ_LOCK(pa_lock);...-1263,101276,10staticvoid__free_pages_ok(structpage*page,unsignedintorder)return;migratetypeget_pfnblock_migratetype(page,pfn);-local_irq_save(flags);local_lock_irqsave(pa_lock,flags);__count_vm_events(PGFREE,1order);free_one_page(page_zone(page),page,pfn,order,migratetype);-local_irq_restore(flags);local_unlock_irqrestore(pa_lock,flags);}这里将local_irq_save()/local_irq_restore()调用替换为local_lock_irqsave()/local_unlock_irqrestore()不再禁用中断使得原本local_irq_save()/local_irq_restore()保护的代码片段可以调度同时由于单个 page是释放到 per-CPU 的 PCP 列表所以 per-CPU 的pa_lock也足以保证数据的正确并发访问。看下local_lock_irqsave()/local_unlock_irqrestore()的实现细节#definelocal_lock_irqsave(lvar,_flags)\do{\if(__local_lock_irqsave(get_local_var(lvar)))\put_local_var(lvar);\_flags__this_cpu_read(lvar.flags);\}while(0)#defineget_local_var(var)(*({\migrate_disable();\this_cpu_ptr(var);}))staticinlineint__local_lock_irqsave(structlocal_irq_lock*lv){if(lv-owner!current){/* 当前进程抢锁 */__local_lock_irq(lv);return0;}else{/* 嵌套上锁 */lv-nestcnt;return1;}}staticinlinevoid__local_lock_irq(structlocal_irq_lock*lv){spin_lock_irqsave(lv-lock,lv-flags);/* 抢锁成功后立即返回失败则进入睡眠直至抢锁成功后返回 */LL_WARN(lv-owner);LL_WARN(lv-nestcnt);lv-ownercurrent;/* 标记为锁的占有者 */lv-nestcnt1;}migrate_disable()调用是为了防止进程睡眠后唤醒调度到其它 CPU 上去这样就无法正确的访问原本所在的 CPU 的 per-CPU 数据了。软中断的 per-softirq 类型 local_irq_lock 锁/* * On RT we serialize softirq execution with a cpu local lock per softirq */staticDEFINE_PER_CPU(structlocal_irq_lock[NR_SOFTIRQS],local_softirq_locks);void__initsoftirq_early_init(void){inti;for(i0;iNR_SOFTIRQS;i)local_irq_lock_init(local_softirq_locks[i]);}staticvoidlock_softirq(intwhich){local_lock(local_softirq_locks[which]);}staticvoidunlock_softirq(intwhich){local_unlock(local_softirq_locks[which]);}staticvoiddo_current_softirqs(void){while(current-softirqs_raised){inti__ffs(current-softirqs_raised);unsignedintpending,mask(1Ui);current-softirqs_raised~mask;local_irq_enable();/* 临时使能中断好了现在可以 softirq 处理上下文可以被抢占了 */.../* * If the lock is contended, we boost the owner to * process the softirq or leave the critical section * now. *//* * 由于 softirq 处理在线程中, 且可抢占, 所以对 i 类型的 softirq * 上锁, 防止多个进程/线程同时处理同一个 softirq 事件. */lock_softirq(i);local_irq_disable();/* 抢到当前 CPU 上 i 类型的 softirq 锁了, 禁用中断处理一个 i 类型的 softirq */softirq_set_runner(i);/* 标记 i 类型的 softirq 的当前处理 softirq 进程 *//* * Check with the local_softirq_pending() bits, * whether we need to process this still or if someone * else took care of it. */pendinglocal_softirq_pending();if(pendingmask){/* 只处理当前 CPU 上的 softirq 事件 */set_softirq_pending(pending~mask);/* 清除 cpu 上挂起的 i 类型的 softirq 事件 */do_single_softirq(i);/* 处理一个 i 类型的 softirq */}/* 处理 i 类型的 softirq 事件完毕, 清除 i 类型的 softirq 的当前处理 softirq 的任务 */softirq_clr_runner(i);WARN_ON(current-softirq_nestcnt!1);local_irq_enable();/* 重启中断 */unlock_softirq(i);/* 解除当前 CPU 对 i 类型的 softirq 的锁定 */local_irq_disable();/* 再次禁用中断准备处理下一 softirq 事件 */}}这里的 softirq 不光有 per-CPU 的 spinlock即local_irq_lock还有 per-CPU 每 softirq 类型的锁这样就不会像本文开头所述会形成一把 per-CPU 的 softirq 大锁而是针对每个 softirq 类型的细粒度锁。把锁的粒度细化这也是常见的优化不必要的锁竞争的技巧之一。从前面分析可以看到local_irq_lock和对 per-CPU 相关的数据访问关系密切。本文对local_irq_lock的讨论就到此为止对更多场景感兴趣的读者可自行阅读源码分析。3. 参考资料[1] Lock types and their rules