内核并发来源:
1、硬件中断和异常:中断服务程序和被中断的进程可能发生并发访问资源
2、软中断和tasklet,软中断和taklet随时都可能倍调度执行,从而打断当前正在执行
进程的上下文。
3、内核抢占:调度器支持可抢占性,会导致进程和进程之间的并发访问。
4、多处理器并发执行,多处理器上可以同时运行多个进程
单处理器:
1、硬中断服务程序可以打断软中断、taklet以及进程上下文
2、软中断和tasklet之间不会并发,但可以打断进程上下文
3、支持抢占式的内核中,进程上下文会并发
4、不支持抢占的内核中,进程上下文不会发生并发。
注意:tasklet 也是一种软中断
多处理器
1、同一类型的中断服务程序不会并发,但是不同类型的中断可能到达不同的cpu上,
所以不同类型的中断服务程序之间可能存在并发执行
2、同一类型的软中断会在不同的cpu上并发执行
3、同一类型的tasklet是串行执行的,不会在多个cpu上并发
4、不同cpu 上的进程上下文会并发
临界区:访问和操作共享数据的代码段
编写代码的要点是:保护资源或者数据,而不是保护代码
比如:静态局部变量、全局变量、共享的内存、数据结构、buffer、链表等
写内核代码时需要作出一些思考:
1、除了当前内核代码路径外是否还有其他内核路径代码会访问它?比如中断服务程序、
工作者worker处理程序、tasklet 处理程序、软中断
2、当前内核代码访问该资源时发生被抢占,被调度执行的进程会不会访问该数据
3、进程会不会睡眠阻塞等待该资源。
linux 内核提供了多种并发访问的保护机制。
1、原子操作和内存屏障
//x86 平台 static inline void atomic_add(int i, atomic_t *v) { asm volatile(LOCK_PREFIX "addl %1,%0" : "+m" (v->counter) : "ir" (i)); } static inline void atomic_sub(int i, atomic_t *v) { asm volatile(LOCK_PREFIX "subl %1,%0" : "+m" (v->counter) : "ir" (i)); }
原子操作的实现:使用了volatile 关键字 还有LOCK_PREFIX (不知道啥:貌似是 lock 锁住总线防止smp 存在访问??)
内存屏障:
解决啥问题?
是cpu指令,
- 保证指令执行的顺序,内存屏障前的指令一定先于内存屏障后的指令,因为cpu和编译器会进行优化而导致指令重排列,单线程情况下,没什么影响,而多线程时,会发生与我们代码执行顺序不一样的结果
- 将write buffer的缓存行,立即刷新到内存中
volatile作用:(#lock前缀)volatile不保证原子性
- 禁止该指令与之前和之后的读和写指令重排序。
- 把写缓冲区中的所有数据刷新到内存中。
- 使其他处理器里的缓存行无效
进行写入操作时,会在写后面加上一条store指令,将本地内存中的共享变量值立即刷新到主存。
在进行读操作时,会在读前面加上一条load指令,从主存中读取变量。
spinlock:
自旋锁是一种非阻塞锁,如果某线程需要获取自旋锁,但该锁已经被其他线程占用时,该线程不会被挂起,而是在不断的消耗CPU的时间,不停的试图获取自旋锁。
typedef struct spinlock { union { struct raw_spinlock rlock; #ifdef CONFIG_DEBUG_LOCK_ALLOC # define LOCK_PADSIZE (offsetof(struct raw_spinlock, dep_map)) struct { u8 __padding[LOCK_PADSIZE]; struct lockdep_map dep_map; }; #endif }; } spinlock_t; typedef struct raw_spinlock { arch_spinlock_t raw_lock; #ifdef CONFIG_GENERIC_LOCKBREAK unsigned int break_lock; #endif #ifdef CONFIG_DEBUG_SPINLOCK unsigned int magic, owner_cpu; void *owner; #endif #ifdef CONFIG_DEBUG_LOCK_ALLOC struct lockdep_map dep_map; #endif } raw_spinlock_t; //arm 架构下 typedef struct { union { u32 slock; struct __raw_tickets { #ifdef __ARMEB__ u16 next; u16 owner; #else u16 owner; u16 next; #endif } tickets; }; } arch_spinlock_t;
static __always_inline void spin_lock(spinlock_t *lock) { raw_spin_lock(&lock->rlock); } #define raw_spin_lock(lock) _raw_spin_lock(lock) static inline void __raw_spin_lock(raw_spinlock_t *lock) { preempt_disable(); spin_acquire(&lock->dep_map, 0, 0, _RET_IP_); LOCK_CONTENDED(lock, do_raw_spin_trylock, do_raw_spin_lock); }
spink_lock 首先做的事是:关闭内核抢占,为什么?--->1、如果sinlock临界区允许抢占,临界区发生中断,中断返回时回去检查抢占调度,那么 会导致抢占调度相当于持有锁的进程睡眠,违背了spinlock锁不能休眠和快速完成的设计语义。2、抢占调度进程也可能回去申请spinlock,这样也会导致死锁。
如果没有打开lockdep 和lock_stat 那么spin_acpuire就是空函数;LOCK_CONTENDED()
只是调用do_raw_spin_lock函数,
https://blog.csdn.net/chenpuo/article/details/78297902
static inline void do_raw_spin_lock(raw_spinlock_t *lock) __acquires(lock) { __acquire(lock); arch_spin_lock(&lock->raw_lock); } static inline void arch_spin_lock(arch_spinlock_t *lock) { unsigned long tmp; u32 newval; arch_spinlock_t lockval; prefetchw(&lock->slock); __asm__ __volatile__( "1: ldrex %0, [%3] " " add %1, %0, %4 " " strex %2, %1, [%3] " " teq %2, #0 " " bne 1b" : "=&r" (lockval), "=&r" (newval), "=&r" (tmp) : "r" (&lock->slock), "I" (1 << TICKET_SHIFT) : "cc"); while (lockval.tickets.next != lockval.tickets.owner) {------ 初始化时lock->tickets.owner、lock->tickets.next
都为0,假设第一次执行arch_spin_lock,lockval = *lock,lock->tickets.next++,lockval.tickets.next
等于lockval.tickets.owner,获取到自旋锁;自旋锁未释放,第二次执行的时候,
lock->tickets.owner = 0, lock->tickets.next = 1,拷贝到lockval后,
lockval.tickets.next != lockval.tickets.owner,会执行wfe等待被自旋锁释放被唤醒,
自旋锁释放时会执行lock->tickets.owner++,lockval.tickets.owner重新赋值 wfe();------ 暂时中断挂起执行,使处理器进入a low-power state等待状态 lockval.tickets.owner = ACCESS_ONCE(lock->tickets.owner);------ 重新读取lock->tickets.owner } smp_mb(); }
嵌入了asm 汇编, __volatile__关键字确保下面代码不被优化改变。
根据嵌入汇编的规则,%0 是lockval ,类型为arch_spinlock_t类型,%1 %2 依次是newval 和tmp,依次类推。
把lock->slock值保存到lock_val
newval=lockval+1<<TICKET_SHIFT TICKET_SHIFT 是 用来表明arch_spinlock_t中owner 和next各自所占多少bit,定义为16则,owner和next各占16bit 加1<<TICKET_SHIFT 就是next++
第3,4行:lock->slock = newval ,判断strex返回值,确认是否成功更新锁,如果更新失败,说明有其他内核路径插入。
ldrex 和strex 用这种方法保证原子性,所以同一时间只会有一个线程能成功更新lock->slock的值。
同样内核中原子操作也是用类似的方法实现。
第5行:如果更新失败回到1行。如果更新成功,到下面c代码
对于单核处理器:
#define _raw_spin_lock(lock) __LOCK(lock) #define __LOCK(lock) do { preempt_disable(); ___LOCK(lock); } while (0) #define ___LOCK(lock) do { __acquire(lock); (void)(lock); } while (0) #ifdef __CHECKER__ …… # define __acquire(x) __context__(x,1) # define __release(x) __context__(x,-1) #else …… # define __acquires(x) # define __releases(x)
对于单核处理器紧紧只是禁止抢占而已
问题:当驱动程序调用spinklock去访问某临界资源以及数据时,在处于临界区时发生了外部硬件中断,此时本地cpu 暂停当前进程去执行中断服务程序, 中断服务程序中spinlock也要访问此临界区域,此时就会造成死锁,所以此时不能使用spinklock
为了解决此场景:需要使用带有关闭本地cpu 中断的spin_lock,也就是spin_lock_irq;
static __always_inline void spin_lock_irq(spinlock_t *lock) { raw_spin_lock_irq(&lock->rlock); } #define raw_spin_lock_irq(lock) _raw_spin_lock_irq(lock) void __lockfunc _raw_spin_lock_irq(raw_spinlock_t *lock) { __raw_spin_lock_irq(lock); } static inline void __raw_spin_lock_irq(raw_spinlock_t *lock) { local_irq_disable();//关闭本地处理器中断 preempt_disable(); spin_acquire(&lock->dep_map, 0, 0, _RET_IP_); LOCK_CONTENDED(lock, do_raw_spin_trylock, do_raw_spin_lock); }
spin_lock_irqsave:保存本地cpu当前irq状态并且关闭本地cpu 中断,然后获取锁,
local_irq_save:保存本地cpu当前irq状态并且关闭本地中断,----local_irq_restore()--->
这样是为了防止破坏中断响应的状态
spin_lock_bh:用于处理进程和延时处理机制(中断下半部)并发访问互斥的问题。
spin_lock的缺点:获取锁不公平 性能下降。
只要和其他CPU 互斥,就要用spin_lock/spin_unlock,如果要和irq及其他CPU互斥,就要用 spin_lock_irq/spin_unlock_irq,
如果既要和irq及其他CPU互斥,又要保存 EFLAG的状态,就要用spin_lock_irqsave/spin_unlock_irqrestore,
如果 要和bh及其他CPU互斥,就要用spin_lock_bh/spin_unlock_bh,
static __always_inline void spin_lock_bh(spinlock_t *lock) { raw_spin_lock_bh(&lock->rlock); } #define raw_spin_lock_bh(lock) _raw_spin_lock_bh(lock) static inline void __raw_spin_lock_bh(raw_spinlock_t *lock) { __local_bh_disable_ip(_RET_IP_, SOFTIRQ_LOCK_OFFSET);//关闭软中断 spin_acquire(&lock->dep_map, 0, 0, _RET_IP_); LOCK_CONTENDED(lock, do_raw_spin_trylock, do_raw_spin_lock); }
如果不需要和 其他CPU互斥,只要和irq互斥,则用local_irq_disable/local_irq_enable,
#define local_irq_disable() do { raw_local_irq_disable(); } while (0) #define raw_local_irq_disable() arch_local_irq_disable() /* * Disable IRQs */ #define arch_local_irq_disable arch_local_irq_disable static inline void arch_local_irq_disable(void) { unsigned long temp; asm volatile( " mrs %0, cpsr @ arch_local_irq_disable " " orr %0, %0, #128 " " msr cpsr_c, %0" : "=r" (temp) : : "memory", "cc"); }
如果不需要和其他CPU互斥,只要和bh互斥,则用local_bh_disable/local_bh_enable,
static inline void local_bh_disable(void) { __local_bh_disable_ip(_THIS_IP_, SOFTIRQ_DISABLE_OFFSET); }
1.有些情况下需要在访问共享资源时必须中断失效,而访问完后必须中断使能,这样的情形使用spin_lock_irq和spin_unlock_irq最好;
2.spin_lock_irqsave保存访问共享资源前的中断标志,然后失效中断;spin_unlock_irqrestore将恢复访问共享资源前的中断标志而不是直接使能中断;
3.如果被保护的共享资源只在进程上下文访问和软中断上下文访问,那么当在进程上下文访问共享资源时,可能被软中断打断,从而可能进入软中断上下文来对被保护的共享资源访问,因此对于这种情况,对共享资源的访问必须使用spin_lock_bh和 spin_unlock_bh来保护。当然使用spin_lock_irq和spin_unlock_irq以及spin_lock_irqsave和 spin_unlock_irqrestore也可以,它们失效了本地硬中断,失效硬中断隐式地也失效了软中断。但是使用spin_lock_bh和 spin_unlock_bh是最恰当的,它比其他两个快。如果被保护的共享资源只在进程上下文和tasklet或timer上下文访问,那么应该使用与上面情况相同的获得和释放锁的宏,因为tasklet和timer是用软中断实现的。
4.对tasklet和timer和互斥操作
如果被保护的共享资源只在一个tasklet或timer上下文访问,那么不需要任何自旋锁保护,因为同一个tasklet或timer只能在一个CPU上运行,即使是在SMP环境下也是如此;如果被保护的共享资源只在两个或多个tasklet或timer上下文访问,那么对共享资源的访问仅需要用spin_lock和spin_unlock来保护,不必使用_bh版本,因为当tasklet或timer运行时,不可能有其他tasklet或timer在当前CPU上运行。
5.spin_lock用于阻止在不同CPU上的执行单元对共享资源的同时访问以及不同进程上下文互相抢占导致的对共享资源的非同步访问,而中断失效和软中断失效却是为了阻止在同一CPU上软中断或中断对共享资源的非同步访问
接口API的类型 | spinlock中的定义 | raw_spinlock的定义 |
定义spin lock并初始化 | DEFINE_SPINLOCK | DEFINE_RAW_SPINLOCK |
动态初始化spin lock | spin_lock_init | raw_spin_lock_init |
获取指定的spin lock | spin_lock | raw_spin_lock |
获取指定的spin lock同时disable本CPU中断 | spin_lock_irq | raw_spin_lock_irq |
保存本CPU当前的irq状态,disable本CPU中断并获取指定的spin lock | spin_lock_irqsave | raw_spin_lock_irqsave |
获取指定的spin lock同时disable本CPU的bottom half | spin_lock_bh | raw_spin_lock_bh |
释放指定的spin lock | spin_unlock | raw_spin_unlock |
释放指定的spin lock同时enable本CPU中断 | spin_unlock_irq | raw_spin_unock_irq |
释放指定的spin lock同时恢复本CPU的中断状态 | spin_unlock_irqstore | raw_spin_unlock_irqstore |
获取指定的spin lock同时enable本CPU的bottom half | spin_unlock_bh | raw_spin_unlock_bh |
尝试去获取spin lock,如果失败,不会spin,而是返回非零值 | spin_trylock | raw_spin_trylock |
判断spin lock是否是locked,如果其他的thread已经获取了该lock,那么返回非零值,否则返回0 | spin_is_locked | raw_spin_is_locked |